将与参数无关的代码抽离templates——条款44

        Templates是节省时间和避免代码重复的一个奇方妙法。不再需要键入20个类似的classes而每一个带有15个成员函数,你只需键入一个class template,留给编译器去具现化那20个你需要的相关classes和300个函数。(class templates的成员函数只有在被使用时才被暗中具现化,所以只有在这300个函数的每一个都被使用,你才会获得这300个函数。)Function templates有类似的诉求。替换写许多函数,你只需要写一个function templates,然后让编译器做剩余的事情。

        有时候,如果你不小心,使用templates可能会导致代码膨胀:其二进制码带着重复(或几乎重复)的代码、数据,或两者。其结果又可能源码看起来合身而整齐,但目标码(object code)却不是那么回事。

        当你编写某个函数,而你明白其中某些部分的实现码和另一个函数的实现码实质相同,你会很单纯地重复这些码码?当然不。你会抽出两个函数的共同部分,把它们放进第三个函数中,然后令原先两个函数调用这个新函数。也就是说,你分析了两个函数,找出共同的部分和变化的部分,把共同部分搬到一个新函数去,保留变化的部分在原函数中不动。

        编写templates时,也是做相同的分析,以相同的方式避免重复,但其中有个窍门。在non-template代码中,重复十分明确:你可以“看”到两个函数或两个classes之间有所重复。然而在template代码中,重复是隐晦的:毕竟只存在一份template源码,所以你必须训练自己去感受当template被具现化多次时可能发生的重复。

        举个例子,假设你想为固定尺寸的正方矩阵编写一个template。该矩阵的性质之一是支持逆矩阵运算。

template<typename T, std::size_t n>
class SquareMatrix {
public:
    ...
    void invert();        // 求逆矩阵
};

        这个template接受一个类型参数T,除此之外还接受一个类型为size_t的参数,那是个非类型参数。这种参数和类型参数比起来较不常见,但它们完全合法,而且就像本例一样,相当自然。

        现在,考虑这些代码:

SquareMatrix<double,5> sml;
...
sm1.invert();                     // 调用SquareMatrix<double,5>::invert
SquareMatrix<double,10> sm2;
...
sm2.invert();                     // 调用SquareMatrix<double,10>::invert

        这回具现化两份invert。这些函数并非完完全全相同,因为其中一个操作的是5*5矩阵而另一个操作的是10*10矩阵,但除了常量5和10,两个函数的其他部分完全相同。这是template引出代码膨胀的一个典型例子。

        如果你看到两个函数完全相同,只除了一个使用5而另一个使用10,你会怎么做?你的本能会为它们建立一个带数值参数的函数,然后以5和10来调用这个带参数的函数,而不重复代码。下面是对SquareMatrix第一次修改:

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;     // 避免遮掩base班的invert;见条款33
public:
    ...
    void invert() { this->invert(n); }    // 制造一个inline调用,调用base class版的invert。
};

        目前为止一切都好,但还有一些棘手的问题没有解决。SquareMatrixBase::invert如何知道该操作什么数据?虽然它从参数中知道矩阵尺寸,但它如何知道哪个特定矩阵的数据在哪儿?想必只有derived class知道。我们可以令SquareMatrixBase贮存一个指针,指向矩阵数值所在的内存。而只要它存储了那些东西,也就可能存储矩阵尺寸。成果看起来像这样:

template<typename T>
class SquareMatrixBase {
protected:
    SquareMatrixBase(std::size_t n, T* pMem)      // 存储矩阵大小和一个指针,指向矩阵数值
    : size(n), pData(pMem) {}
    void setDataPtr(T* ptr) { pData = ptr; }      // 重新赋值给pData
    ...
private:
    std::size_t size;                             // 矩阵大小
    T* pData;                                     // 指针,指向矩阵内容
};

        这允许derived classes决定内存分配方式。某些实现版本也许会决定将矩阵数据存储在SquareMatrix对象内部:

template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
    SquareMatrix()                       // 送出矩阵大小和数据指针给base class
    : SquareMatrixBase<T>(n, data) {}
    ...
private:
    T data[n*n];
};

        这种类型的对象不需要动态分配内存,但对象自身可能非常大。另一种做法是每一个矩阵的数据放进heap(也就是通过new来分配内存):

template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
    SquareMatrix()                     // 将base class的数据指针设为null,为矩阵内容分配内存
    : SquareMatrixBase<T>(n, 0),       
    pData(new T[n*n])                      // 将指向该内存的指针存储起来,
    { this->setDataPtr(pData.get()); }     // 然后将它的一个副本交给base class
    ...
private:
    bosst::scoped_array<T> pData; 
};

        这个条款只讨论由non-type template parameters(非类型模板参数)带来的膨胀,其实type parameters(类型参数)也会导致膨胀。例如在许多平台上int和long有相同的二进制表述,所以像vector<int>和vector<long>的成员函数有可能完全相同——这正是最佳定义。

请记住

  • Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
  • 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可以消除,做法是以函数参数或class成员变量替换template参数。
  • 因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述的具现类型共享实现码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值