减少模板中的代码膨胀

来源:互联网 发布:各个协议的端口号 编辑:程序博客网 时间:2024/05/29 12:08

   在非模板代码中,重复十分明确:你可以“看”到两个函数或两个class之间有所重复。然而在模板代码中,重复是隐晦的:毕竟只存在一份模板源码,所以你必须培养自己去感受当模板被具现化多次时可能发生的重复。

   例如:下面的模板

template<typename T,size_t n>        //template支持n * n矩阵,元素类型为T的对象

class SquareMatrix

{

  public:

    ...

    void invert();                  //求逆矩阵

};

 

    这个模板接受一个类型参数T,除此之外还接受一个类型为size_t的参数,那是个非类型参数。这种参数和类型参数比起来较不常见,但是完全合法。

看如下代码:

SquareMatrix<double,5> sm1;

...

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

SquareMatrix<double,10> sm2;

...

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

   这会具现化两份invert。这些函数并非完完全全相同,因为其中一个操作的是5*5矩阵而另一个操作的是10*10矩阵,但除了常量5和10,两个函数的其他部分完全相同。这是模板引出代码膨胀的一个典型例子。怎么办呢?本能是建立一个带数值参数的函数,然后以5和10来调用这个带参数的函数,而不重复代码。所以下面是对SquareMatrix的第一次修改:

template<typename T>                   //与尺寸无关的基类

class SquareMatrixBase

{

  protected:

   ...

   void invert(size_t maxtrixSize);    //以给定尺寸求逆矩阵

   ...        

};

template<typename T,size_t n>

class SquareMatrix : private SquareMatrixBase<T>

{

   private:

     using SquareMatrixBase<T>::invert;    //避免遮掩base版的invert

   public:

     ...

     void invert(){this->invert(n);}      //一个inline调用,调用基类版的invert。

};

 

  带参数的invert位于基类SquareMatrixBase中,和SquareMatrix一样,SquareMatrixBase也是个模板,不同的是它只对“矩阵元素对象的类型”参数化,不对矩阵的尺寸参数化。因此对于某给定之元素对象类型,所有矩阵共享同一个(也是唯一一个)SquareMatrixBase类。它们也将因此共享这唯一一个类内的invert。

  SquareMatrixBase::invert只是企图成为“避免派生类代码重复”的一种方法,所以它以protected替换public。调用它而造成的额外成本应该是0,因为派生类的invert调用基类版本时用的是inline调用。这些函数使用“this->”记号,因为若不这样做,模板化基类(如SquareMatrixBase<T>)内的函数名称会被派生类覆盖(见上一篇博文)。注意SquareMatrix和SquareMatrixBase之间的继承关系是private,这反映了一个事实:这里的基类只是为了帮助派生类实现,不是为了表现SquareMatrix和SquareMatrixBase之间的is-a关系。

  目前为止一切都好,但还是有一些棘手的问题没有解决。SquareMatrixBase::invert如何知道该操作什么数据?虽然它从参数中知道矩阵尺寸,但它如何知道哪个特定矩阵的数据在哪儿?想必只有派生类知道。派生类如何联络其基类做逆运算动作?一个可能的做法是为SquareMatrixBase::invert添加另一个参数,也许是个指针,指向一块用来放置矩阵数据的内存起点。那可以,但十之八九invert不是唯一一个可写为“形式与尺寸无关并可移至SquareMatrixBase内”的SquareMatrix函数。如果有若干这样的函数,我们唯一需要做的就是找出保存矩阵元素的那块内存。我们可以对所有这样的函数添加一个额外参数,却得一次又一次地告诉SquareMatrixBase相同的信息,这样也不好。

    另一办法是令SquareMatrixBase存储一个指针,指向矩阵数值所在的内存,而只要它存储了那些东西,也就可能存储了矩阵尺寸(注意:这个只用在派生类到基类的构造函数中传递一次矩阵数值而上面的方法需要在每个函数中传递一次。)。看起来像这样:

template<typename T>

class SquareMatrixBase

{

  protected:

    SquareMatrixBase(size_t n, T* pMem)     //存储矩阵大小和一个指针,指向矩阵数值

    :size(n),pData(pMem){}

    void setDataPtr(T* ptr){pData = ptr;}   //重新赋值给pData

    ...

 private:

    size_t size;                            //矩阵大小

    T* pData;                               //指针,指向矩阵的内容

};

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

tempalte<typename T,size_t n>

class SquareMatrix : private SquareMatrixBase<T>

{

   public:

     SquareMatrix():SquareMatrixBase<T>(n,data);  //送出矩阵大小和数据指针给基类

     ...

   private:

     T data[n * n];

};

 

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

template<typename T,szie_t n>

class SquareMatrix : private SquareMatrixBase<T>

{

  public:

    SquareMatrix():SquareMatrixBase<T>(n,0)       //将基类的数据指针设为null

    ,pData(new T[n * n])                          //为矩阵内容分配内存,将指向该内存的指针存储起来

    {this->setDataPtr(pData.get());}              //然后将它的一个副本交给基类

    ...

  private:

    boost::scoped_array<T> pData;

};

   (scoped_array是一个non-TR1智能指针,是个auto_ptr-like智能指针,用来动态分配数组)。

无论数据存储于何处,从膨胀的角度检讨之,关键是现在许多——说不定是所有——SquareMatrix成员函数可以单纯地以inline方式调用基类版本,后者由“持有相同型元素”(无论矩阵大小)之所有矩阵共享。在此同时,不同大小的SquareMatrix对象有着不同的类型,所以即使(例如SquareMatrix<double,5>和SquareMatrix<double,10>)对象使用相同的SquareMatrix<double>成员函数,我们也没机会传递一个SquareMatrix<double,5>对象到一个期望获得SquareMatrix<double,10>的函数去。非常棒,但是必须付出代价。硬是绑着矩阵尺寸的那个invert版本,有可能生成比共享版本(其中尺寸乃以函数的参数传递或存储在对象内)更佳的代码。例如在尺寸专属版中,尺寸是个编译期常量,因此可以藉由常量的传达达到最优化,包括把它们折进被生成指令中成为直接常数。这在“与尺寸无关”的版本中时无法办到的。

  另一个角度,不同大小的矩阵只拥有单一版本的invert,可减少执行文件大小,也就因此降低程序的工作集大小,并强化指令高速缓存区内的引用集中化。这些都可能使程序执行得更快速,超越“尺寸专属版”invert的最优化效果。哪一个占主导地位?欲知答案,唯一办法是两者都尝试并观察你的平台的行为以及面对代表性数据组时的行为。

   上面所说都是由非类型模板参数带来的代码膨胀,其实类型参数也会带来代码膨胀。例如在许多平台上int和long有相同的二进制表述,所以像vector<int>和vector<long>的成员函数有可能完全相同——这正是代码膨胀的最佳定义。某些链接器会合并完全相同的函数实现码,但有些不会,后者意味某些模板被具现化为int和long两个版本,并因此造成代码膨胀(在某些环境下)。类似情况,在大多数平台上,所有指针类型都有相同的二进制表述,因此凡模板持有指针者(例如list<int*>,list<const int*>,list<SquareMatrix<long,3>*>等等)往往应该对每一个成员函数使用唯一一份底层实现。这很具有代表性地意味,如果你实现某些成员函数而它们操作强型指针(即T*),你应该令它们调用另一个操作无类型指针(即void*)的函数,由后者完成实际工作。某些C++标准程序库实现版本的确为vector,deque和list等模板做了这件事。如果你关心你的模板可能出现代码膨胀,也许你会想让你的模板也做相同的事情。

 

 总结:(1)模板生成多个类和多个函数,所以任何模板代码都不该与某个造成膨胀的模板参数产生相依关系。

       (2)因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数或类成员变量替换template参数

       (3)因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述的具现类型共享实现码。

0 0
原创粉丝点击