annidy / notes

0 stars 0 forks source link

avoid C++ Template explode by example of a ServiceCenter #199

Open annidy opened 7 months ago

annidy commented 7 months ago

从入门到精通:如何解决C++模板代码膨胀问题?

All of the story start from this architecture

// 所有 Service 需要实现的接口
class BaseService {
public:
    virtual ~BaseService() = default;
    virtual void onServiceInit() = 0;
    ……    
    std::string contextName{};
};
// 所有 Service 单例的统一管理中心
class ServiceCenter {
public:
    explicit ServiceCenter(const std::string& name) : _contextName(name) {}

    template<typename T>
    std::shared_ptr<T> getService() {
        ……
    }

private:
    std::unordered_map<std::string, std::shared_ptr<BaseService>> _serviceMap = {};
    std::string _contextName;
    std::recursive_mutex _mutex;
};

getService is a template function, this is the first implemnt

class ServiceCenter {
public:
    template<typename T>
    std::shared_ptr<T> getService() {
        auto const key = typeid(T).name();
        std::lock_guard<std::recursive_mutex> lock(_mutex);
        auto const itr = _serviceMap.find(key);
        if (itr == _serviceMap.end()) {
            return nullptr;
        }
        auto service = itr->second;
        return std::dynamic_pointer_cast<T>(service);
    }
};

first, use typeid(T).name() to get the name, and use std::dynamic_pointer_cast to get safe smart pointer conversion. Looks like it support base class type. The result is a shared_ptr, so this ServiceCenter can be free in app life time, cool!

I'm a bit doubtful, typeid return an base class name

The map find has no relationship with type, so let it out

class ServiceCenter {
public:
    std::shared_ptr<BaseService> getService(const std::string &key) {
        std::lock_guard<std::recursive_mutex> lock(_mutex);
        auto const itr = _serviceMap.find(key);
        if (itr == _serviceMap.end()) {
            return nullptr;
        }
        return itr->second;
    }

    template<typename T>
    std::shared_ptr<T> getService() {
        auto const key = typeid(T).name();
        auto service = getService(key);
        return std::dynamic_pointer_cast<T>(service);
    }
};

now getService is shorter than before. But what if we want create the service if not get

class ServiceCenter {
public:
    template<typename T>
    std::shared_ptr<T> getService() {
        std::lock_guard<std::recursive_mutex> lock(_mutex);

        auto const key = typeid(T).name();
        auto const service = getService(key);
        if (service == nullptr) {
            auto const tService = std::make_shared<T>();
            tService->contextName = _contextName;
            setService(key, tService);
            tService->onServiceInit();
            return tService;
        } else {
            auto const tService = std::dynamic_pointer_cast<T>(service);
            if (tService == nullptr) {
                aerror("ServiceCenter", "tService is null");
                return nullptr;
            }
            return tService;
        }
    }

    void setService(const std::string &key, const std::shared_ptr<BaseService> &service) {....}
};

Yes, use auto const tService = std::make_shared<T>(); to create. But now this function a litter bigger, can we abstract more. So far, there are 3 places use type T.

  1. typeid(T).name()
  2. std::make_shared()
  3. std::dynamic_pointer_cast()

we make a haler class to do this jobs

class ServiceTypeHelperBase {
public:
    virtual ~ServiceTypeHelperBase() = default;
    virtual const char* getTypeName() const = 0;
    virtual std::shared_ptr<BaseService> newInstance() const = 0;
    virtual std::shared_ptr<void> castToOriginType(std::shared_ptr<BaseService> service) const = 0;
};

template<typename T>
class ServiceTypeHelper : public ServiceTypeHelperBase {
    const char* getTypeName() const override {
        return typeid(T).name();
    }
    std::shared_ptr<BaseService> newInstance() const override {
        return std::make_shared<T>();
    }
    std::shared_ptr<void> castToOriginType(std::shared_ptr<BaseService> service) const override {
        return std::dynamic_pointer_cast<T>(service);
    }
};

Pretty clear, we has a class ServiceTypeHelperBase, so there is can avoid type T. Now we can move all these code into non-template function as before.

class ServiceCenter {
public:
    std::shared_ptr<void> getService(const ServiceTypeHelperBase* helper) {
        std::lock_guard<std::recursive_mutex> lock(_mutex);

        auto const key = helper->getTypeName();
        auto const service = getService(key);
        if (service == nullptr) {
            auto const tService = helper->newInstance();
            tService->contextName = _contextName;
            setService(key, tService);
            tService->onServiceInit();
            return helper->castToOriginType(tService);
        } else {
            auto const tService = helper->castToOriginType(service);
            if (tService == nullptr) {
                aerror("ServiceCenter", "tService is null");
                return nullptr;
            }
            return tService;
        }
    }

    template<typename T>
    std::shared_ptr<T> getService() {
        ServiceTypeHelper<T> helper;
        auto service = getService(&helper);
        return std::static_pointer_cast<T>(service);
    }
};

Fantastic, you may notice that ServiceTypeHelperBase.castToOriginType() return a std::shared_ptr<void>, we must use std::static_pointer_cast to convert back! Sometimes we need to compromise on type safety and speed.

Because ServiceCenter is not a singleton, If we has multi instance, template function will make every instance very big. We can upgrade template class function to template class, to make each object has smaller code segment

// 业务相关的 BaseService 协议
class BussinessBaseService : public BaseService {
public:
    BussinessBaseService(const std::string& name) : _bussinessName(name) {}
    virtual void onBussinessEnter() = 0;
    virtual void onBussinessExit() = 0;
    ……
protected:
    const std::string _bussinessName;
};
// 业务相关的 ServiceCenter
template <typename BaseService_t>
class ServiceCenter {
public:
    explicit ServiceCenter();

public:
    void setService(const std::string &key, const std::shared_ptr<BaseService> &service);
    std::shared_ptr<BaseService> getService(const std::string &key);

    template<typename T>
    std::shared_ptr<T> getService() {
        static_assert(std::is_base_of<BaseService_t, T>::value, "Wrong Service Type");
        ……
    }
……
private:
    std::unordered_map<std::string, std::shared_ptr<BaseService>> _serviceMap = {};
    std::string _bussinessName;
    std::string _contextName;
    std::recursive_mutex _mutex;
};
// 例如朋友圈业务 Service 基类
class TLBussinessServiceBase : public BussinessBaseService {
    TLBussinessServiceBase(const std::string& name) : BussinessBaseService(name) {}
    void onBussinessEnter() override { /*some common logic*/ }
    void onBussinessExit() override { /*some common logic*/ }
    ……
};
// 朋友圈 ServiceCenter,要求所有 Service 都继承自 TLBussinessServiceBase
static ServiceCenter<TLBussinessServiceBase> g_tlServiceCenter;

As you see, If we want make smaller code size, we must abstract many middle layers, reduce the compiler generate duplicate code, by a function invoke.

siya100 commented 6 months ago

hey @annidy can you please guide me to solve this