45.将参数无关的代码抽离template

前言

 
Templates是避免代码重复与节约时间的最优解。但有时,template可能会造成代码膨胀:二进制码带着大量重复(或几乎重复)的代码或者数据。其源码可能看起来优雅整洁,但实际上生成的object code完全不是一回事,我们需要对此进行优化。


共性与变性分析

 
当我们在撰写函数时,往往倾向于提取出函数的共同部分,组成新的函数,然后令原来的函数调用该新函数。在OOP中,如果多个class存在相同部分,则会抽离classes的共有部分组成新的class,然后采用继承或者复合获取共性,而原本classes的互异部分则保持不变。
在编写templates也是同样的方式避免重复,但在细节处略有不同。在non-template程序中我们可以明确地看到重复的函数或重复的class成员,但在template编码中,源码只有一份,必须自己去感知当template具现化时可能发生的重复。

实例分析

为一个方阵编写template,其性质之一是支持求逆:

1
2
3
4
5
template<typename T,size_t n>
class SquareMatrix{
public:
void invert();//求逆矩阵
}

假设现有操作如下:
1
2
3
4
SquareMatrix<double,5> sm1;
sm1.invert();
SquareMatrix<double,10> sm2;
sm2.invert();

以上操作直接导致具现化了两个invert,虽然它们并非完全相同,但是相当类似,唯一的区别就是一个作用于5*5矩阵一个作用于10*10矩阵。

解决方案

第一版修改方案

当我们看到两个除了参数之外完全相同的函数,本能反应应当是建立一个带数值参数的可调用函数,而不是在class中重复代码。基于这种理念,第一版修改方案如下:

1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
class SquareMatrixBase{
protected:
void invert(size_t matrixSize);
}
template<typename T,size_t n>
class SquareMatrix:private SquareMatrixBase{
private
using base::invert;//避免遮蔽名称
public:
void invert() {this->invert(n);}//inline调用
}

如你所见,所有矩阵元素类型相同的class共有一个bc,并且共享这唯一一个class内的invert.在dc中调用invert的额外成本应该为0,因为这是一个inline调用,this->的使用详见前一节内容(Effective C++ 44)。private继承说明了bc只是为了帮助dc实现,他们并不是is-a的关系。


第二版修改方案

现在的问题是:base内的invert如何知道操作什么数据?虽然它知道尺寸,但他如何知道某个特定矩阵的数据存在于内存的何处?这显然只有dc知道。dc如何联络bc做逆运算呢?​
一个可能的做法是向invert函数添加另一个参数,一个指向矩阵数据的指针。根据经验,invert应该不是唯一一个需要获取矩阵数据的函数,所以我们令base class贮存一个指针,指向矩阵数值指向的内存,既然它持有了数据,那必然也需要了解矩阵的大小。具体实现如下所示:

1
2
3
4
5
6
7
8
9
template<typename T>
class SquareMatrixBase{
protected:
SquareMatrixBase(size_t n,T *pMem):size(n),pData(pMem) {}
void setDataPtr(T* ptr) {pData = ptr;}//更改数据域
private:
size_t n;
T* pData;//矩阵具体内容
}

这种写法允许derived classes自主决定内存分配方式。

dc的内存分配方式

  1. 矩阵存储于对象内部
    1
    2
    3
    4
    5
    6
    7
    template<typename T,size_t n>
    class SquareMatrix:private SquareMatrixBase<T>{
    public:
    SquareMatrix():SquareMatrixBase<T>(n,data) {}
    private:
    T data[n*n];
    };
    这一类对象无需动态分配内存,但是对象自身可能非常大。
  2. 把对象数据放入heap
    1
    2
    3
    4
    5
    6
    7
    8
    template<typename T,size_t n>
    class SquareMatrix:private SquareMatrixBase<T>{
    public:
    SquareMatrix():SquareMatrixBase<T>(n,0),padta(new T[n*n])
    {this->setDataPtr(pData.get());}
    private:
    shared_ptr<T> pData;//RAII
    };

总结

  1. template生成多个class与多个函数,所以任何template代码都不应该与某个造成膨胀的template参数相互依存
  2. 因为non-type template parameters造成的代码膨胀往往可以消除,做法是以函数参数或者class成员变量替换template参数。
  3. 因type parameters造成的代码膨胀往往可以降低,做法是让带有相同二进制表述的具现类型共享实现码。