读书笔记 effective c++ Item 44 将与模板参数无关的代码抽离出来

简介: 1. 使用模板可能导致代码膨胀 使用模板是节省时间和避免代码重用的很好的方法。你不需要手动输入20个相同的类名,每个类有15个成员函数,相反,你只需要输入一个类模板,然后让编译器来为你实例化20个特定的类和300个你需要的函数。

1. 使用模板可能导致代码膨胀

使用模板是节省时间和避免代码重用的很好的方法。你不需要手动输入20个相同的类名,每个类有15个成员函数,相反,你只需要输入一个类模板,然后让编译器来为你实例化20个特定的类和300个你需要的函数。(只有在被使用的情况下类模版的成员函数才会被隐式的实例化,所以只有在300个函数被实际用到的情况下才会生成300个成员函数。)函数模板同样吸引人。你不用手动实现许多函数,你只需要实现一个函数模板,然后让编译器来做余下的事情。

然而在有些时候,如果你不小心,使用模板会导致代码膨胀(code bloat):产生重复代码或者数据的二进制文件,或者两者都有。结果可能是源码看起来合身整齐,但是目标代码(object code)臃肿松弛。臃肿松弛很不好,因此你需要知道如果避免这样的二进制浮夸。

2. 共性和可变性分析

 你的主要工具有着很威风的名字:共性和可变性分析(commonality and variability analysis),但是这个概念很平常。即使在你的编程生涯中从未实现过一个模板,你也总是会做这样的分析。

2.1 函数和类中的代码重复分析

当你正在实现一个函数,你意识到函数实现的某些部分同另外一个函数实现基本上是相同的 ,你会重复这些代码么?当然不会。你将两个函数的公共代码提取出来,放进第三个函数中,然后在两个函数中调用这个新函数。总结一下就是,你对两个函数进行分析,找到相同和不同的部分,将相同的部分移到一个新的函数中去,将不同的部分保留在原来的函数中。类似的,如果你正在实现一个类,你意识到类中的一部分另一个类中的一部分是相同的,你不应该重写相同的部分。相反,你可以将相同的部分移到一个新类中,然后使用继承或者组合(Item 32,Item 38,Item 39)让原始类访问共同的特性。原始类中不同的部分仍然保留在原来的位置。

2.2 模板中的代码重复分析及消除重复方法

当实现模板的时候,你也会做相同的分析,你会使用相同的方式来阻止重复,但是这里有一个让你伤痛的地方。在非模板(non-template)代码中,重复是显示的:你可以看到在函数之间或者类之间会有代码重复。在模板代码中,重复是隐式的:只有一份模板源码,所以你必须训练你自己当一个模板被实例化多次的时候,你能够感觉到重复会不会发生

2.2.1 消除代码膨胀第一关——去掉非类型参数

例如,假设你想为固定大小的矩阵实现一个模板,需要支持矩阵的转置。

1 template<typename T, // template for n x n matrices of
2 std::size_t n> // objects of type T; see below for info
3 class SquareMatrix { // on the size_t parameter
4 public:
5 ...
6 
7 void invert();                         // invert the matrix in place
8 
9 };     

                                     

这个模板带了一个类型参数,T,但是也带了一个类型size_t的参数,一个非类型(non-type)参数。非类型参数比类型参数少了共性,但是它们是完全合法的,并且在这个例子中,它们也能非常自然。

现在考虑下面的代码:

 1 SquareMatrix<double, 5> sm1;
 2 
 3 ...
 4 
 5 sm1.invert();
 6 
 7 // call SquareMatrix<double, 5>::invert
 8 
 9 SquareMatrix<double, 10> sm2;
10 
11  
12 
13 ...
14 
15  
16 
17 sm2.invert();
18 
19 // call SquareMatrix<double, 10>::invert
20 
21  

 

在这里将会实例化invert的两份拷贝。这两个函数并不相同,因为一个在5*5的矩阵上工作,另外一个在10*10的矩阵上工作,但是如果不考虑常量5和10,这两个函数将会是一样的。这是使得包含模板的代码出现膨胀的典型方式。

如果你看到两个函数,它们的所有字符都是相同的,除了一个版本使用5而另外一个版本使用10,你接下来会做什么?你的直觉是会创建一个带一个参数的函数版本,然后以5或者10为入参调用这个函数而不是重复代码。你的直觉能够很好的为你服务!这是实现SquareMatrix的第一关:

 1 template<typename T> // size-independent base class for
 2 class SquareMatrixBase { // square matrices
 3 protected:
 4 ...
 5 void invert(std::size_t matrixSize); // invert matrix of the given size
 6 ...
 7 };
 8 template<typename T, std::size_t n>
 9 class SquareMatrix: private SquareMatrixBase<T> {
10 private:
11 using SquareMatrixBase<T>::invert; // make base class version of invert
12 // visible in this class; see Items 33
13 // and Item 43
14 public:
15 ...
16 void invert() { invert(n); } // make inline call to base class
17 }; // version of invert

 

正如你所看到的,带参数的invert版本被放在基类SquareMatrixBase中。像SquareMatrix一样,SquareMatrixBase是一个模板,但是与SquareMatrix不同的是,它在矩阵中只对对象类型进行模板化。因此,包含一个给定类型对象的所有矩阵将会分享一个单一的SquareMatrixBase类。这样它们会分享SquareMatrixBase类的invert版本的单一拷贝。(你不能将其声明为inline,因为一旦被inline了,每个SquareMatrix::invert的实例都会得到SquareMatrixBase::invert代码的一份拷贝(看Item 30),你会发现你有回到了对象代码重复的原点。)

SquareMatrixBase::invert只被用来在派生类中防止代码重复,所以是protected而不是public的。调用它的额外开销应该是0,因为派生类的inverts调用基类版本使用了inline函数。(inline是隐式的 见Item 30)同时注意SquareMatrix和SquareMarixBase之间的继承是private的。这精确的反映出一个事实:使用基类的唯一原因是帮助派生类的实现,并非表达出SquareMatrixSquareMatrixBase之间的“is-a”关系。(有关private继承的信息,见Item 39

2.2.2 消除代码膨胀第二关——派生类如何告知基类数据在哪里

到现在为止看上去都很好,但是还有一个我们没有处理的棘手的问题。SquareMatrixBase::invert如何知道在什么数据上进行操作?它从参数中得知矩形的大小,但是它如何知道为特殊矩阵提供的数据在哪里?大概只有派生类才会知道。派生类如何同基类进行通讯才能让基类执行invert?

一个可能的方法是向SquareMatrixBase::invert中添加另外一个参数,可能是一个指向一块内存的指针,内存中存放矩形数据。这种方法可以工作,但是十有八九,invert不是存在于SquareMatrix中的能够以独立于size的方式重写的,并且移入SquareMatrixBase中的唯一函数。如果有几个这样的函数,我们就需要一种方法能够找到存放矩形数据的内存,我们可以为所有的函数添加一个额外的参数,但是如此以来我们就重复告诉了SquareMatrixBase同样的信息。这看上去是错误的。

一个替换方法是让SquareMatrixBase存储一个指向存放矩形数据的内存的指针。这同存放矩形大小有相同的效果。结果如下:

 1 template<typename T>
 2 class SquareMatrixBase {
 3 protected:
 4 SquareMatrixBase(std::size_t n, T *pMem) // store matrix size and a
 5 : size(n), pData(pMem) {} // ptr to matrix values
 6 
 7 void setDataPtr(T *ptr) { pData = ptr; }   // reassign pData
 8 
 9 ...                                                            
10 
11 private:                                                  
12 
13  
14 
15 std::size_t size;           // size of matrix
16 
17 T *pData;       // pointer to matrix values
18 
19 
20 };

 

这就让派生类来决定如何分配内存。一些实现会在SquareMatrix对象内部存储矩形数据:

1 template<typename T, std::size_t n>
2 class SquareMatrix: private SquareMatrixBase<T> {
3 public:
4 SquareMatrix() // send matrix size and
5 : SquareMatrixBase<T>(n, data) {} // data ptr to base class
6 ...
7 private:
8 T data[n*n];
9 };

 

这种类型的对象没有必要做动态内存分配,但是对象本身可能会非常大。一个替换的方法是为每个矩形在堆上存放数据:

 1 template<typename T, std::size_t n>
 2 class SquareMatrix: private SquareMatrixBase<T> {
 3 public:
 4 SquareMatrix() // set base class data ptr to null,
 5 : SquareMatrixBase<T>(n, 0), // allocate memory for matrix
 6 pData(new T[n*n]) // values, save a ptr to the
 7 { this->setDataPtr(pData.get()); } // memory, and give a copy of it
 8 
 9 ...                                              // to the base class
10 
11 private:
12 boost::scoped_array<T> pData;          // see Item 13 for info on
13 
14  
15 
16 };                                   // boost::scoped_array

 

2.2.3 消除代码膨胀前后效率对比

不管将数据存放在哪里,从代码膨胀的角度来说,关键结果是现在很多(可能是所有的)SquareMatrix的成员函数可以简单的inline调用基类的(non-inline)函数版本,所有持有相同类型数据的矩形共享基类中的函数,不管size是多少。同时,不同size的SquareMatrix对象属于不同类型,所以即使SquareMatrix<double,5>和SquareMatrix<double,10>对象在SquareMatrixBase<double>中使用相同的成员函数,把一个SquareMatrix<double,5>对象传给一个需要SquareMatrix<double,10>的函数是没有机会的。好还是不好呢。

好是好,但是需要付出代价。矩形size大小固定的invert版本比按函数参数传递size大小(或者存储在对象中)的invert版本可能产生更好的代码。例如,在指定size的版本中,sizes是编译期常量,因此是常量传播优化的合格者,也可以把其放入生成指令中作为直接操作数。这在同size无关的版本中无法做到。

从另外一个方面,为不同size的矩阵只提供一个invert版本可以减小可执行程序的大小,这能减少程序的工作集大小,并且能够强化指令高速缓存的引用集中化。这些东西能够使得程序运行速度更快,并且相对size指定的版本才能做出的优化,它可能会做出更好的补偿。哪种方法效果更好?唯一的方法是两种方法都试一下,在你的特定平台和有代表性的数据集上观察它们的行为。

另外一个有关效率的需要考虑的地方是有关对象的大小。如果你不介意,将size大小无关的版本向上移动到基类中会增加每个对象的大小。例如,在我刚刚展示的代码中,每个SquareMatrix对象有一个指向SquareMatrixBase类中数据的指针。即使每个派生类中已经有取得数据的方法,这也为每个SquareMatrix对象至少增加一个指针的大小。我们可以修改设计来去掉指针,但是这也是需要付出代价的。例如,让基类存储一个指向数据的protected指针,但会导致封装性的降低(Item 22).它同样能导致资源管理并发症:如果基类存储了指向矩阵数据的指针,但是数据既有可能是动态分配的也可能存储在派生类对象中(正如我们看到的),如何决定是不是需要delete指针?这样的问题是有答案的,但是你做的越精细事情就变得越复杂。从某种意义上讲,有一点代码重复开始开起来有点幸运了。

2.3 如何处理类型模板参数导致的代码膨胀

这个条款仅仅讨论了由于非类型模板参数导致的代码膨胀,但是类型参数同样可以导致代码膨胀。例如,在许多平台中,int和long有着相同的二进制表示,所以在成员函数中使用vector<int>和vector<long>看起来会一样,这正是代码膨胀的定义。一些连接器会把相同的代码实现整合到一起,但是有一些不会,这就意味着由模板实例化的int和long版本会在一些环境中导致代码膨胀。类似的,在大多数平台上,所有的指针类型有着相同的二进制表示,所以带指针类型的模板(例如,list<int*>,list<const*>,list<SquareMatrix<long,3>*>等等)应该通常能够为每个成员函数使用一个单一的底层实现。特别的,这就意味着实现一个强类型指针(T* 指针)的成员函数时,让它们调用一个无类型指针的函数(void*指针)。一些标准C++库的实现为模板就是这么做的(如vector,deque,和list)。如果你关心在你的模板中出现的代码膨胀问题,你可能就会想开发出做相同事情的模板。

3. 总结

  • 模板会产生多个类和多个函数,所以任何模板不应该依赖于会导致代码膨胀的模板参数。
  • 非类型模板参数导致的代码膨胀通常情况下可以将模板参数替换为函数参数或者类数据成员来清除。
  • 由类型参数导致的代码膨胀也可以被降低,方式是为实例化类型共享相同的二进制表示。


作者: HarlanC

博客地址: http://www.cnblogs.com/harlanc/
个人博客: http://www.harlancn.me/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出, 原文链接

如果觉的博主写的可以,收到您的赞会是很大的动力,如果您觉的不好,您可以投反对票,但麻烦您留言写下问题在哪里,这样才能共同进步。谢谢!

目录
相关文章
|
5月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
9月前
|
存储 算法 安全
c++模板进阶操作——非类型模板参数、模板的特化以及模板的分离编译
在 C++ 中,仿函数(Functor)是指重载了函数调用运算符()的对象。仿函数可以像普通函数一样被调用,但它们实际上是对象,可以携带状态并具有更多功能。与普通函数相比,仿函数具有更强的灵活性和可扩展性。仿函数通常通过定义一个包含operator()的类来实现。public:// 重载函数调用运算符Add add;// 创建 Add 类的对象// 使用仿函数return 0;
287 0
|
9月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
234 0
|
12月前
|
编译器 C++
模板(C++)
本内容主要讲解了C++中的函数模板与类模板。函数模板是一个与类型无关的函数家族,使用时根据实参类型生成特定版本,其定义可用`typename`或`class`作为关键字。函数模板实例化分为隐式和显式,前者由编译器推导类型,后者手动指定类型。同时,非模板函数优先于同名模板函数调用,且模板函数不支持自动类型转换。类模板则通过在类名后加`&lt;&gt;`指定类型实例化,生成具体类。最后,语录鼓励大家继续努力,技术不断进步!
|
安全 C++
【c++】模板详解(2)
本文深入探讨了C++模板的高级特性,包括非类型模板参数、模板特化和模板分离编译。通过具体代码示例,详细讲解了非类型参数的应用场景及其限制,函数模板和类模板的特化方式,以及分离编译时可能出现的链接错误及解决方案。最后总结了模板的优点如提高代码复用性和类型安全,以及缺点如增加编译时间和代码复杂度。通过本文的学习,读者可以进一步加深对C++模板的理解并灵活应用于实际编程中。
202 0
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
11月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
440 12
|
9月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
373 0
|
设计模式 安全 C++
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
223 16
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)