七、模板与泛型编程--条款44-48
条款44: 将与参数无关的代码抽离template
如果使用template不当,可能会导致代码膨胀(code bloat):其二进制码带着重复(或几乎重复)的代码、数据、或两者。但是源码看起来合身而整齐,目标码却不是那么一回事了。
例如一个求逆矩阵的类:
template<typename T, size_t n>
class SquareMatrix
{
public:
void invert();
};
SquareMatrix<double ,5> sm1, sm2;
...
sm1.invert();
这就是引出代码膨胀的一个例子,一个操作的是5*5矩阵,一个是10 * 10,两个常量不同,就会具现化出两份invert函数。
如何修改这个类?
我们每次要求不同矩阵的时候,都会具现化出多份不同的函数。所以我们的目标就是只具现化出一份。那么就给invert一个参数就行了,然后各自求逆矩阵去调用它即可:
template<typename T>
class SquareMatrixBase
{
protected:
void invert();
};
template<typename T, size_t n>
class SquareMatrix : public SquareMatrixBase
{
private:
using SquareMatrixBase<T>::invert;
public:
void invert()
{
this->invert(n);
}
};
这样的话,就可以只具现化出一份代码。
上面的只是伪代码而已,真正可以运行的代码,肯定要把当前矩阵内容传递给基类的invert函数。否则只知道大小不知道矩阵值,怎么计算逆矩阵呢?
作者总结
Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
因非类型模板参数而造成的代码膨胀,往往可以消除,做法是以函数参数或class成员变量替换template参数。
因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述的具现类型共享实现码。
条款45:运用成员函数模板接受所有兼容类型
我们在使用指针的时候,是支持隐式转换的。Derived class指针可以隐式转换为base class指针,指向non-const对象的指针可以转换为const对象等等。
但是同一个template的不同具现体之间并不存在与生俱来的固有关系。我们要运用成员函数模板接受所有的兼容类型。
例如一个智能指针类,我们要支持拷贝构造,运用一个不同类型的智能指针,再去获取原生指针,最后再放到返回的智能指针类中:
template<typename T>
class SmartPtr
{
public:
template<typename U>
SmartPtr(const SmartPtr<U> &other); // 泛化copy构造函数
{
// 通过other获取原生指针
// 再放到我们的这个类中返回
}
};
作者总结
请使用member function templates(成员函数模板)声成“可接受所有兼容类型”的函数。
如果你声明member templates用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。
条款46: 需要类型转换时请为模板定义非成员函数
还记得我们讲过的一个重载乘号的例子吗?对,就是那个不要返回引用的条款。
template<typename T>
class Rational
{
public:
Rational(const T numberator = 0, const T denominator = 1);
const T numberator(); // 获取分子
const T denominator(); // 获取分母
private:
T numberator;
T denominator;
};
template<typename T>
const Rational<T> operator*(const Rational<T> &lhs,onst Rational<T> &rhs)
{
...
}
还是这个类,只是现在它是个类模板。这样还能完成原有的功能吗?
Rational<int> oneHalf(1,2); // 正确
Rational<int> result = oneHalf * 2; // 编译错误
被弄糊涂的编译器
如果我们要调用正确的operator * ,具现化出接受两个参数为Rational
问题在于我们所写的代码,使得编译器无法推导出来T的类型。在这里编译器会这样判断:
(1) 首先,调用operator * ,第一参数是oneHalf,编译器可以正确推导出来T的类型:因为oneHalf的类型是Rational
(2) 第二参数是个int,编译器要怎么通过int推导出T呢?这是行不通的。
你可能希望编译器能够通过隐式转换将2转换成Rational
正确使用friend函数来解决问题
我们只需做一个小小的改进:将原来的template function声明为friend的,并将定义放置在类中即可。
template<typename T>
class Rational
{
public:
Rational(const T numberator = 0, const T denominator = 1);
const T numberator(); // 获取分子
const T denominator(); // 获取分母
friend const Rational operator*(const Rational &lhs,onst Rational &rhs)
{
return Rational(lhs.numberator() * rhs.numberator(),
lhs.denominator() * rhs.denominator());
}
private:
T numberator;
T denominator;
};
接下来说明为什么要声明成friend以及为什么要把定义都放在类里面。
(1) 声明为friend函数是为了成为non-member成员函数之后,还能够在这个类模板里面声明,得以正确的具现化出两个T的类型。当oneHalf对象被声明出来之后,在类模板内,T的类型就被确定了:int. 此时我们的函数在类模板内,也就是说,不仅是第一个参数,第二个参数一样的T也是int类型。 而不在类模板里面,编译器只能通过调用的实参进行推导,又由于无法进行隐式转换,所以编译不过。
(2) 倘若我们只是进行friend函数的声明,定义不在类内。编译可以通过,链接的时候无法链接,原因在于编译器找不到函数的定义。编译的时候我们得知T的类型,使得我们的语句可以找到对应函数,编译器认为这并没有问题。但是运行的时候,外部的template operator * 怎么会知道是内部函数的那个T呢 ?所以我们还是需要同时将定义也写进去。
作者总结
当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。
条款48:认识模板元编程
模板元编程(Template metaprogramming),最大的特地在于执行与编译期,并且:
- 它让某些事情更容易。
- 将工作从运行期转为编译期,使得某些原本需要运行期才能找到的错误在编译器就能够找出来。
- 较小的执行文件,较短的运行期,较少的内存需求。
- 同时,需要较长的编译时间。
对于模板元编程,这里(包括书上)也只是做了简单的讨论,本人认为模板元编程需要看更多的材料或者做一些相关的类型编程,才能养成这种意识。 同样的,这里采用书上的例子进行模板元编程的引入,就好比如hello world一样。
阶乘算法:
template<unsigned n>
struct func
{
enum
{
value = n * func<n - 1>::value
};
};
template<>
struct func<0>
{
enum
{
value = 1
};
};
这样就完成一个计算阶乘的模板元编程做法。若在main函数中执行:
cout << func<5>::value << endl;
则可计算出结果为120.
个人觉得模板元编程还是很酷的,通过巧妙的递归写法,能够在编译期就发现问题,为什么要等到代码上线了才发现问题呢?
作者总结
Template metaprogramming可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
TMP可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。