Effective C++条款44:模板与泛型编程之(将与参数无关的代码抽离templates)

一、代码重复与对应策略

对于函数而言

  • 假设我们编写某些函数(不止一个),这些函数当有有部分代码都是相同的,因此我们认为这份代码对于程序来说是重复的
  • 那么此时我可以将这些函数共同的部分抽取出来,然后放在另一个函数中,让原本那些函数调用这个新函数。这样就达到了缩减代码的效果

对于类而言

  • 假设我们在编写一组类,而这些类中有些部分都是相同的,因此我们认为这份代码对于程序来说是重复的
  • 那么此时我们可以将这些类中共同的部分抽取出来,建立一个新类,然后让原本那些类继承于这个新类。这样就达到了缩减代码的效果

对于template(函数模板/类模板)来说

  • 我们知道只有当template被使用时才会进行代码的实例化,并且针对每一个实例化,在代码中都会生成相对应的代码,因此针对于每一个实例化,它们之间也可能会产生代码重复的现象
  • 对于普通函数/类来说,代码重复是很容易看出来的(通过代码)。但是对于template来说,其是不容易发现的,因为只有template被实例化时,你才知道template产生了什么内容

二、演示案例

  • 假设现在我们有这样一个类模板,其用来表示固定尺寸的正方矩阵,该矩阵还支持一个逆矩阵运算的方法。代码如下:
//矩阵的元素类型为T,矩阵的大小为n*n类型(模板第二个类型为非类型参数)
template<typename T, std::size_t n>
class SquareMatrix {
public:
    void invert(); //求逆矩阵
};
  • 假设我们有下面的调用,那么对于invert()函数的调用来说可能会产生代码膨胀。理由如下:
    • 下面的5*5矩阵和10*10矩阵都调用了invert(),该函数是用来对矩阵进行逆转的
    • 但是根据invert()的实现代码来看,其除了矩阵的大小不一样以外,invert()其他的代码对于矩阵的处理都是相同的,因此上面生成的两个模板会产生代码重复的地方
SquareMatrix<double,5> sm1;
sm1.invert();

SquareMatrix<double,10> sm2;
sm2.invert();

第一次修改

  • 因为invert()中的代码会产生重复,因此我们现在建立下面的继承体系。代码如下:
    • 我们修改了SquareMatrix类模板名为SquareMatrixBase,使其只接受矩阵类型,为invert()添加一个参数,用来表示来处理的矩阵大小
    • 添加一个SquareMatrix类模板继承于它,让其接受一个size_t模板参数用来表示处理的矩阵的大小
//不论SquareMatrix产生多少粉,SquareMatrixBase在代码中只产生一份
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; //避免派生类隐藏基类的invert函数
public:
    void invert() {
        this->invert(n); //为什么使用this,参阅条款43
    }
};
  • 此处相比于起初节约代码的原因:
    • 当SquareMatrix实例化时,其并不会产生太多的代码,因为其直接调用基类中的invert函数
    • 并且由于基类中的invert不会因为矩阵大小不同而实例化不同的模板类了,因为其生成一份模板类代码,根据矩阵大小的不同,调用其中的invert()函数即可
  • 一些语法注意事项:
    • 因为SquareMatrixBase的功能交给派生类实现了,因此将SquareMatrixBase中的invert()函数声明为protected,不允许外界直接操作SquareMatrixBase
    • SquareMatrix使用private继承于SquareMatrixBase,因为其用到的技术是前面介绍过的“implemented-terms-of(根据某物实现出)”的技术(可以参阅条款38、39)
    • 另外,在SquareMatrix的invert()中对于基类invert()的调用使用了this指针(这一块内容可以参阅条款43)

第二次修改

  • 现在还有问题:
    • SquareMatrixBase()用来完成对矩阵的处理,但是SquareMatrixBase如何知道处理的这些数据来自于哪里呢?
    • 因此SquareMatrix()必须使用一种方法,将所操作的数据传递给SquareMatrixBase()
  • 解决方法:
    • ①(不可取的方法):为SquareMatrixBase()::invert()提供一个指针参数,用来表示要处理的矩阵的数据。这样行的通,电商部不可取,因为SquareMatrixBase()整个类都需要操作这块数据,如果SquareMatrixBase()中有很多成员方法,那么就需要为每一个成员方法都提供这一个指针,因此这种方法不可取
    • ②(本次修改所操作的方法):在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;         //指向矩阵的内容
};
  • 派生类的代码如下:其将自己存储矩阵的实际数据的指针传递给基类处理,代码如下:
template<typename T, std::size_t n>
class SquareMatrix :private SquareMatrixBase<T>
{
    //其他同上
public:
    SquareMatrix():
        SquareMatrixBase<T>(n, data) {} //将存储矩阵数据的指针传递给基类
private:
    T data[n*n]; //存储矩阵的实际数据
};

第三次修改

  • 紧接着“第二次修改”,上面我们将矩阵数据存储在SquareMatrix类中可能会造成SquareMatrix对象自身非常大
  • 另一种做法是:把矩阵的数据存储在堆上。代码如下:
template<typename T, std::size_t n>
class SquareMatrix :private SquareMatrixBase<T>
{
public:
    SquareMatrix():
        SquareMatrixBase<T>(n, 0), pData(new T[n*n])
    {
        //将它的一个副本交给base class
        this->setDataPtr(pData.get());
    }
private:
    boost::scoped_array<T> pData;
};
  • 经过三次修改,代码已经相对于比较优化了,具体细节在于:
    • SquareMatrix的成员函数会单纯地以inline方式调用SquareMatrixBase版本
    • SquareMatrixBase类由“持有相同元素类型但矩阵大小不同的”SquareMatrix类对象所共享
    • 并且我们为SquareMatrixBase添加了一个setDataPtr()函数,所以当我们操作不同的SquareMatrix对象时(例如SquareMatrix<double,5>和SquareMatrix<double,10>),不会担心某个对象会操作另外一个对象的数据,因为我们使用了setDataPtr()设置了SquareMatrixBase中的矩阵数据指针
  • 关于代码与效率的几个说明:
    • ①SquareMatrix的invert()函数,可能比共享版本(SquareMatrixBase的)的invert()函数产生更佳的代码。例如在SquareMatrix中,矩阵大小是个编译器常量,因此可以藉由常量的广传达到最优化,包括把它们折进被生成指令中成为直接操作数。这在SquareMatrixBase中是无法办到的
    • ②不同大小的矩阵只拥有单一版本的invert()(SquareMatrixBase中的),可减少执行文件大小,也就因此降低程序的work set大小,并强化指令告诉缓存区内的引用集中化。这些都可能使程序执行得更快速
      • 所谓work set是指对一个在“虚内存环境”下执行的进程而言,其所使用的那一组内存页
    • ③另一个效能评比所关心的主题是对象大小:我们在SquareMatrix中存储了一个指针,指向于矩阵的数据,因此SquareMatrix锁创建的对象大小会增加。
      • 另一种做法:去除SquareMatrix中的指针,让SquareMatrixBase存储一个protected指针指向矩阵数据。但是这会导致丧失封装性,另外可能导致资源管理上的混乱和复杂(参阅条款22)
      • 因此,我们在SquareMatrix存储指针,虽然增加了一些空间,但是从利与弊来说,这是值得的

三、附加

  • 本条款是讨论由non-type template parameters(非类型模板参数)带来的膨胀,其实type parameters(类型参数)也会导致膨胀

例如

  • 许多平台上int和long有着相同的二进制表述
  • 所以vector<int>和vector<long>的成员函数有可能完全相同——造成代码重复/膨胀
  • 有些链接器(linkers)会合并相同的函数实现码,但有些不会
  • 因此某些模板被具体化为int和long两个版本,那么就造成代码重复/膨胀

例如

  • 在大多数平台上,所有指针类型都有着相同的二进制表述
  • 因此如果一个templates持有指针,例如list<int*>、list<const int*>、list<SquareMatrix<long,3>*>等等,往往应对每一个成员函数使用唯一一份底层实现
  • 折痕具代表性地意味,如果你实现某些成员函数而它们操作强型指针(即T*),你应该令它们调用另一个操作无类型(即void*)指针的函数,由后者完成实际工作
  • 某些C++标准程序库实现版本的确为vector、deque、list等templates做了这些事情。如果你关心你的templates可能出现代码膨胀,也会你会想让你的templates也使用这些技术

四、总结

  • Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系
  • 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数
  • 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述的具现类型共享实现码
发布了1525 篇原创文章 · 获赞 1084 · 访问量 45万+

猜你喜欢

转载自blog.csdn.net/qq_41453285/article/details/104856465
今日推荐