在使用模板时,编译器会为每一组不同的模板参数生成一份独立的实例化代码。如果这些代码中存在与参数无关的部分,就会导致生成的二进制文件冗余,增加内存占用和指令缓存压力。
1. 核心问题:代码膨胀 (Code Bloat)
模板虽然能减少源代码的重复,但如果不加节制,会导致编译后的二进制代码重复。
举个例子:固定大小的矩阵
假设我们要写一个表示正方形矩阵的类,其中矩阵大小是一个非类型模板参数(non-type template parameter):
template<typename T, std::size_t n>
class SquareMatrix {
public:void invert(); // 求逆矩阵
};// 使用
SquareMatrix<double, 5> sm1;
SquareMatrix<double, 10> sm2;
问题在于:
编译器会为 SquareMatrix<double, 5>::invert 和 SquareMatrix<double, 10>::invert 生成两份几乎完全相同的机器码。虽然矩阵大小 $n$ 不同,但求逆算法的逻辑(如高斯消元)对于 double 类型通常是一致的。这种由非类型参数引起的重复就是代码膨胀。
2. 解决方案:因式分解 (Factorization)
解决思路类似于代数中的公因式提取:将不依赖于特定模板参数的代码提取到基类或独立函数中。
第一步:引入带有参数的基类
我们将大小 $n$ 从模板参数改为函数参数,并放入一个基类中:
template<typename T>
class SquareMatrixBase {
protected:void invert(std::size_t matrixSize); // 提取出来的公共逻辑
};template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
private:using SquareMatrixBase<T>::invert; // 避免遮掩基类名称
public:void invert() { this->invert(n); } // 调用基类版本,传入大小
};
这样,无论 $n$ 是 5 还是 10,底层都只调用同一份 SquareMatrixBase<double>::invert 代码。
第二步:处理数据指针
上面的 invert 仍然需要知道矩阵数据在哪里。我们不希望在基类中写死数组大小,因此可以使用指针:
template<typename T>
class SquareMatrixBase {
protected:SquareMatrixBase(std::size_t n, T* pMem) : size(n), pData(pMem) {}void setDataPtr(T* ptr) { pData = ptr; }std::size_t size;T* pData; // 矩阵数据的指针void invert() { /* 使用 size 和 pData 进行计算 */ }
};template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:SquareMatrix() : SquareMatrixBase<T>(n, data) {}
private:T data[n*n]; // 数据存储在派生类中
};
3. 两种膨胀来源的应对策略
条款 44 主要讨论了两种导致膨胀的情况:
A. 非类型模板参数 (Non-type Parameters)
如上面的 size_t n。
- 做法: 将该参数替换为函数参数或类成员变量。
- 收益: 减少因数值不同导致的实例化副本。
B. 类型参数 (Type Parameters)
如 T。虽然不同类型(如 int 和 double)通常需要不同的二进制代码,但某些类型在底层表示上是相同的。
- 例子: 很多平台上,所有指针类型的底层实现是完全一样的(如
vector<int*>和vector<Shape*>)。 - 做法: 某些高级实现(如早期 STL)会让所有指针类型的模板共用一个
void*的实现版本,通过强转来保证类型安全。
4. 权衡与代价
虽然抽离代码能显著减小二进制体积,但也存在权衡:
- 性能微降: 抽离后的代码可能无法利用编译器针对特定数值(如固定的 $5 \times 5$ 循环)进行的硬编码优化(Inline 或 Loop Unrolling)。
- 复杂性: 引入基类增加了代码层级,数据指针的管理也需要更加小心(防止野指针)。
- 内存开销: 在基类中存储指针或大小会略微增加每个对象的内存占用。
5. 核心结论
- 警惕膨胀: 模板会产生重复的代码和数据。
- 共性提取: 如果发现多个模板实例化后的行为基本一致,应通过共性分析将无关代码移入基类。
- 非类型参数转变量: 将非类型模板参数(如 $n$)改为构造函数参数或成员变量,往往是消除冗余的第一步。