前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从入门到精通:如何解决C++模板代码膨胀问题?

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

作者头像
微信终端开发团队
发布2024-02-21 13:56:33
2310
发布2024-02-21 13:56:33
举报

作者:guoling,来自微信客户端团队

前言

  • 背景:C++ 模板是一种强大的编程工具,它允许我们编写通用的、可重用的代码;
  • 问题:模板代码的一个常见问题是代码膨胀,即编译器为每个模板实例生成大量的重复代码。现代的编译器已经能够对不同编译单元里的相同模板函数进行去重,老生常谈的 external 模板、将模板代码与非模板代码分离等,对瘦身意义已经不大,我们仍然需要关注如何减少每一个模板实例化的大小。

除了显而易见的减少实例化类型的数量(实际业务场景下其实大部分减不了),「本文主要是提供适用于一些具体场景、可实际操作的优化策略以减少C++模板代码的大小。」

策略说明

主要包括:

  1. 模板函数:提取通用部分
  2. 模板类:抽象出通用部分到基类
  3. 合理使用模板
  4. 小技巧:多用组合、避免使用大型对象等等。

1. 将模板函数的通用部分提取出来

如果模板函数中有一部分代码与模板参数无关,那么可以将这部分代码提取出来,放到一个非模板函数中。这样,这部分代码只需要生成一次,而不是在每个模板实例中都生成一次。 为了方便讨论,后续的例子基于这个场景:我们提供一个集中的 Service 单例管理器。以下是大体的框架:

代码语言:javascript
复制
// 所有 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;
};

1.1 最简单的情形,函数大部分逻辑都是跟模板参数无关:

例如,在我们的例子中,getService() 函数最简单的版本可能长这样,显然,一大部分代码是与模板参数无关的,可以提取出来:

代码语言:javascript
复制
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);
    }
};

我们抽出一个非模板的函数getService(const std::string &key),将加锁、查询 map 这些逻辑都挪进去,优化后:

代码语言:javascript
复制
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);
    }
};

1.2 稍复杂的情形,函数大部分逻辑都跟模板参数有关:

例如,getService()函数不但要管查询,还要按需创建新实例、初始化、以及各种异常处理,有3行代码都用了类型T:

代码语言:javascript
复制
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<AffBaseService> &service) {....}
};

这种情况我们可以将需要用到类型T的地方进行封装,抽象出一套协议。具体来说,用到T的地方分别有:

  • typeid(T).name(): 可抽象出接口getTypeName(),通过 RTTI 返回类型的名字。
  • std::make_shared(): 考虑到对tService接下来的操作都是已在基类BaseService里定义的接口,可抽象出接口newInstance(),返回基类指针。
  • std::dynamic_pointer_cast(): 这里主要是将基类指针动态地转换为子类指针,可抽象出接口castToOriginType(),在里面进行类型转换,返回一个void类型的指针。

使用最常见的多态(也可用 C 函数指针数组std::function<>传参等)来组织这 3 个抽象接口:

代码语言:javascript
复制
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);
    }
};

然后就可以抽出非模板的函数getService(const ServiceTypeHelperBase* helper)。可见到,得益于合理的抽象,新函数跟没优化之前的getService()几乎一模一样:

代码语言:javascript
复制
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);
    }
};

注意,抽象出来的接口必须「足够精简,避免换个地方写模板函数」;太复杂的话,起不到瘦身效果。例如下面这样,就是不够精简的抽象:

代码语言:javascript
复制
// 反面例子,不要这样做
template<typename T>
class ServiceTypeHelper : public ServiceTypeHelperBase {
    const char* getTypeName() const override {
        return typeid(T).name();
    }
   std::shared_ptr<BaseService> newInstance() const override {
        auto const tService = std::make_shared<T>();
        tService->contextName = _contextName;
        setService(key, tService);
        tService->onServiceInit();
        return tService;
    }
   std::shared_ptr<void> castToOriginType(std::shared_ptr<BaseService> service) const override {
        auto const tService = std::dynamic_pointer_cast<T>(service);
        if (tService == nullptr) {
            aerror("ServiceCenter", "tService is null");
            return nullptr;
        }
        return tService;
    }
};

2. 将模板类的通用部分提取到基类

❝特别注意:这里的基类指「非模板基类」,或者「模板参数比子类少的基类」;否则只是换个地方写模板类,起不到瘦身效果。 ❞

编译器每实例化一个模板类,会将类的所有部分都复制一份,包括非模板成员变量、模板成员变量、非模板函数、模板函数。尤其是「非模板成员变量和非模板函数,也会复制生成一份」,即使它们没有用到模板信息。这是很多人都会忽视的地方。因此,将通用部分提取到基类,避免编译器重复生成同样的代码,就成了瘦身的有效手段。 为了方便讨论,依然以 ServiceCenter 来举例。假设我们每个大业务(朋友圈、视频号等)都有自己的一套 XXXBaseService,定制了一些大业务相关的通用处理逻辑,因此他们希望有业务专门的 XXXServiceCenter,大概是这么一个架构:

代码语言:javascript
复制
// 业务相关的 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;

2.1 将非模板成员变量和非模板函数提取到基类

这个是不言自明的机械操作:

代码语言:javascript
复制
class BaseServiceCenter {
public:
    void setService(const std::string &key, const std::shared_ptr<BaseService> &service);
    std::shared_ptr<BaseService> getService(const std::string &key);
    
protected:
    std::unordered_map<std::string, std::shared_ptr<BaseService>> _serviceMap = {};
    std::string _contextName;
    std::recursive_mutex _mutex;
};
    
template <typename BaseService_t>
class ServiceCenter : BaseServiceCenter {
public:
    template<typename T>
    std::shared_ptr<T> getService() {
        ……
    }
……
private:
    std::string _bussinessName;
};

2.2 将模板函数抽出通用部分,挪到基类

我们先来看新版getService(),跟之前的有点不一样,Service 构造函数多一个参数:

代码语言:javascript
复制
template <typename BaseService_t>
class ServiceCenter : BaseServiceCenter {
public:
    template<typename T>
    std::shared_ptr<T> getService() {
        static_assert(std::is_base_of<BaseService_t, T>::value, "Wrong Service Type");
        std::lock_guard<std::recursive_mutex> lock(_mutex);
    
        auto const name = typeid(T).name();
        auto const service = getService(name);
        if (service == nullptr) {
            auto const tService = std::make_shared<T>(_bussinessName);  // 这里不一样
            tService->contextName = _contextName;
            setService(name, 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;
        }
    }
};

因此我们抽象出来的 XXXHelper::newInstance() 也需要多一个参数:

代码语言:javascript
复制
class BussinessServiceTypeHelperBase : public ServiceTypeHelperBase {
public:
   std::shared_ptr<BaseService> newInstance() const override {
        return nullptr;
    }
    virtual std::shared_ptr<BussinessBaseService> newInstance(const std::string& name) const = 0;
};
template<typename T>
class BussinessServiceTypeHelper : public BussinessServiceTypeHelperBase {
    const char* getTypeName() const override {
        return typeid(T).name();
    }
    std::shared_ptr<BussinessBaseService> newInstance(const std::string& name) const override {
        return std::make_shared<T>(name);
    }
    std::shared_ptr<void> castToOriginType(std::shared_ptr<BaseService> service) const override {
        return std::dynamic_pointer_cast<T>(service);
    }
};

然后就可以类似 1.2 那样抽出共有逻辑,并挪到基类:

代码语言:javascript
复制
class BaseServiceCenter {
public:
    std::shared_ptr<void> getService(const BussinessServiceTypeHelperBase* helper, const std::string& bussinessName) {
        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(bussinessName);
            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 BaseService_t>
class ServiceCenter : BaseServiceCenter {
public:
    template<typename T>
    std::shared_ptr<T> getService() {
        static_assert(std::is_base_of<BaseService_t, T>::value, "Wrong Service Type");
        BussinessServiceTypeHelper<T> helper;
        auto service = getService(&helper, _bussinessName);
        return std::static_pointer_cast<T>(service);
    }
};

2.3 抽离(多模板参数)子类的共用部分,挪到(少模板参数的)基类

如果基类也有模板参数,那么应尽量使基类的模板参数比子类少,并把子类的共用部分挪到基类。例如,假设现在有如下子类和基类,T 的实例个数是 n,U 的实例个数是 m,那么子类的每个成员变量和成员函数都会「生成 n*m 份」;如果把子类里只与 T 相关的成员挪到基类,那么这些成员「只会生成 n 份」,少了一个数量级。更详细的分析可参考 Effective C++ 44:将参数无关代码重构到模板外去。

代码语言:javascript
复制
template<typename T, typename U>
class Pair {
public:
   std::string getFirstTypeName() const {
        auto typeName = typeid(T).name();
        int status;
        auto demangledName = abi::__cxa_demangle(typeName, 0, 0, &status);
        if (!demangledName) {
            return typeName;
        }
        std::string result = demangledName;
        free(demangledName);
        return result;
    }
    
    T first;
    U second;
};

上面的例子里,getFirstTypeName()明显跟参数U无关,因此可抽出一个基类,并把该函数挪过去:

代码语言:javascript
复制
template<typename T>
class PairLeft {
public:
   std::string getTypeName() const {
        auto typeName = typeid(T).name();
        int status;
        auto demangledName = abi::__cxa_demangle(typeName, 0, 0, &status);
        if (!demangledName) {
            return typeName;
        }
        std::string result = demangledName;
        free(demangledName);
        return result;
    }
};

template<typename T, typename U>
class Pair : private PairLeft<T> {
    using Base = PairLeft<T>;
public:
   std::string getFirstTypeName() const {
        return Base::getTypeName();
   }
};

❝还可以进一步将函数 PairLeft::getTypeName() 抽离出模板无关的部分,放到一个非模板基类里。具体可参考前文,不再赘述。 ❞

3. 合理使用模板

不要为了用模板而用模板。这里举一个具体的反面例子(github上某开源库):一个提供了类似上述 ServiceCenter 功能的库,它侧重点是控制反转 Inversion of Control,亮点是对构造函数依赖的参数的自动查找和构建,以及类型映射。我们学习一个库,除了学它好的地方,也要看到它不好的地方,引以为鉴。

3.1 多余的模板参数

场景1:基类 RegistrationDescriptorBase<TDescriptor, TDescriptorInfo>有两个模板参数,仔细看代码,会发现压根没在基类用过。而这个会导致非常严重的代码膨胀,每个<TDescriptor, TDescriptorInfo>组合就会生成一套全新的基类。

场景2:工具类 EnforceBaseOf<TDescriptorInfo, TBase, T>里面的TDescriptorInfo参数在判断T是否继承自TBase时,完全没用,不知为何要加这么一个参数。

3.2 臃肿的模板组合

这个形容词我想了很久,没找一个合适的词去形容,因为实在太震撼。具体是这个类AutowireableConstructorRegistrationDescriptor:

代码语言:javascript
复制
template <class TDescriptorInfo>
class AutowireableConstructorRegistrationDescriptor : public RegistrationDescriptorBase< AutowireableConstructorRegistrationDescriptor< TDescriptorInfo >, TDescriptorInfo >,
                                                      public RegistrationDescriptorOperations::As< AutowireableConstructorRegistrationDescriptor< TDescriptorInfo >, TDescriptorInfo >,
                                                      public RegistrationDescriptorOperations::OnActivated< AutowireableConstructorRegistrationDescriptor< TDescriptorInfo >, TDescriptorInfo >,
                                                      public RegistrationDescriptorOperations::SingleInstance< AutowireableConstructorRegistrationDescriptor< TDescriptorInfo >, TDescriptorInfo >,
                                                      ……
{
    ……
};

以及这个类RegistrationDescriptorInfo:

代码语言:javascript
复制
template
<
    class T,
    InstanceLifetimes::InstanceLifetime Lifetime = InstanceLifetimes::Transient,
    class TSelfRegistrationTag = Tags::NotSelfRegistered,
    class TFallbackRegistrationTag = Tags::DefaultRegistration,
    class TRegisteredBases = MetaMap<>,
    class TDependencies = MetaMap<>
>
struct RegistrationDescriptorInfo
{
    typedef T InstanceType;
    typedef std::integral_constant< InstanceLifetimes::InstanceLifetime, Lifetime > InstanceLifetime;
    typedef TSelfRegistrationTag SelfRegistrationTag;
    typedef TFallbackRegistrationTag FallbackRegistrationTag;
    typedef TRegisteredBases RegisteredBases;
    typedef TDependencies Dependencies;

    struct SingleInstance
    {
        typedef RegistrationDescriptorInfo
        <
            InstanceType,
            InstanceLifetimes::Persistent,
            SelfRegistrationTag,
            FallbackRegistrationTag,
            RegisteredBases,
            Dependencies
        >
        Type;
    };
    template <class TBase>

    struct RegisterBase
    {
        typedef RegistrationDescriptorInfo
        <
            InstanceType,
            InstanceLifetime::value,
            SelfRegistrationTag,
            FallbackRegistrationTag,
            typename MetaInsert< RegisteredBases, MetaPair< TBase, MetaIdentity< TBase > > >::Type,
            Dependencies
        >
        Type;
    };
    template <class TBase>
    struct IsBaseRegistered : MetaContains< RegisteredBases, TBase >
    {
    };
    ……
};

这两个模板类组合起来,可以提供类似下面这样的语法:

代码语言:javascript
复制
auto contain_builder = Hypodermic::ContainerBuilder();
contain_builder.registerType<TServiceImp>()
    .template as<TServiceInterface>()
    .template as<TServiceBase>()
    .singleInstance()
    .onActivated(
        [](Hypodermic::ComponentContext &ctx, const std::shared_ptr<TServiceImp> &service) {
            // some onActive logic
        }
    );

看起来挺干爽的,有什么问题?

  • 所有「调用信息和调用顺序」,都通过模板参数在RegistrationDescriptorInfo记录下来,这就意味着每多一步操作,就多了一个模板组合。就拿上面这个链式调用来说,一共有 5 个函数调用,也就生成了若干套RegistrationDescriptorInfo、AutowireableConstructorRegistrationDescriptor。调用量越多,生成的类型越多,二进制大小线性增长。
  • 基类用子类作为模板参数 RegistrationDescriptorBase< AutowireableConstructorRegistrationDescriptor< TDescriptorInfo >, TDescriptorInfo >。结合前面 4.1 说的,基类压根就没用上这两个模板参数,进一步加剧了生成类型的数量。
  • 为了使得新增的调用信息对 contain_builder可见,contain_builder需要注册registrationDescriptorUpdated()通知,在回调里处理新的调用信息。代码架构非常复杂混乱。

问题如此严重,那要怎么优化?回头看作者的用心,大概或许应该是防止用户出错。例如映射的基类并不是基类、重复映射同一个基类、重复设置singleInstance,等等「可以在编译期发现的错误」。如果抛开这个大设定,其实所有配置信息都可以用一个 POD (Plain Old Data) 结构体来记录,最后再一次性生效。在 POD 结构体的基础上,我们再来看哪些是可以零成本在编译期完成的错误检查:

  • 「映射的基类不是基类」:这个可以零成本在编译器实现,加一个static_assert<>即可。
  • 「重复映射同一个基类」:这个其实可以在运行时规避/处理,我们用一个unordered_set来记录已映射的基类即可。退一万步说,把“映射同一个基类”做成幂等操作就行了,重复映射一万次都没关系。
  • 「重复设置singleInstance」:同上,用一个 bool 标志位记录即可,以最后一次调用为准。

其他接口如named(), asSelf(), with()等等,也都可参考上面做法,分别在编译期/运行时解决,不再赘述。

4. 小技巧

4.1 多用组合,少用继承

通常来说,多用组合少用继承是设计模式上的好建议,它能提高灵活性、减低耦合度、增强代码复用、减少继承层次。但其实它还有瘦身意义上的好处。假设我们有一个模板类 GraphicObject,它有两个模板参数:Shape 表示形状类型,Color 表示颜色类型。

代码语言:javascript
复制
template<typename Shape, typename Color>
class GraphicObject {
public:
    // ...
};

如果我们有很多不同的 Shape 和 Color 类型,那么 GraphicObject 的每种组合都会生成一个新的模板实例,这可能会导致生成的代码量非常大。

为了减少模板实例化的大小,我们可以将 Shape 和 Color 类型的处理逻辑分离出来,使它们成为 GraphicObject 的成员,而不是模板参数。这样,GraphicObject 就不再需要为每种 Shape 和 Color 类型的组合生成一个新的模板实例,从而减少了模板实例化的大小。

代码语言:javascript
复制
class ShapeBase {
public:
    // ...
};

class ColorBase {
public:
    // ...
};

class GraphicObject {
public:
    GraphicObject(std::shared_ptr<ShapeBase> shape, std::shared_ptr<ColorBase> color)
        : shape_(shape), color_(color) {}
    // ...
private:
    std::shared_ptr<ShapeBase> shape_;
    std::shared_ptr<ColorBase> color_;
};

4.2 避免在模板函数中使用大型对象

模板函数中的对象会在每个模板实例中都生成一份,因此应该避免在模板函数中使用大型对象。如果必须使用大型对象,可以考虑使用指针或引用,或者将对象移动到函数外部。

4.3 善用手边工具,多测量监控代码膨胀问题

Linux 平台下(包括Android),可用 nm 打印出每个符号的大小并按大小排序:

代码语言:javascript
复制
nm --print-size --size-sort xxx_binary

在 macOS/iOS 平台下,可通过生成 LinkMapFile.txt 来进行分析。

❝需要注意的是,现代编译器能够对不同编译单元的相同模板函数进行去重,所以需要对最终链接产物(可执行文件 / 动态库)进行测量,不要对单个 .o .a 文件进行分析。 ❞

优化效果

上述描述的策略目前正逐步应用到微信客户端内进行优化,目前的优化效果是:「将有24个 Service 的代码库从14M瘦身到11M,减少体积22%,效果非常明显。」

总结

总的来说,优化C++模板代码的关键是减少每个模板实例的大小,本文描述的优化策略可以帮助我们提高编译速度,减小生成的二进制文件大小,同时保持代码的可读性和可维护性,完整总结如下:

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-02-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WeMobileDev 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 策略说明
  • 1. 将模板函数的通用部分提取出来
    • 1.1 最简单的情形,函数大部分逻辑都是跟模板参数无关:
      • 1.2 稍复杂的情形,函数大部分逻辑都跟模板参数有关:
      • 2. 将模板类的通用部分提取到基类
        • 2.1 将非模板成员变量和非模板函数提取到基类
          • 2.2 将模板函数抽出通用部分,挪到基类
            • 2.3 抽离(多模板参数)子类的共用部分,挪到(少模板参数的)基类
            • 3. 合理使用模板
              • 3.1 多余的模板参数
                • 3.2 臃肿的模板组合
                • 4. 小技巧
                  • 4.1 多用组合,少用继承
                    • 4.2 避免在模板函数中使用大型对象
                      • 4.3 善用手边工具,多测量监控代码膨胀问题
                      • 优化效果
                      • 总结
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档