七、模板与泛型编程--条款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是什么

问题在于我们所写的代码,使得编译器无法推导出来T的类型。在这里编译器会这样判断:

(1) 首先,调用operator * ,第一参数是oneHalf,编译器可以正确推导出来T的类型:因为oneHalf的类型是Rational,所以T是int。

(2) 第二参数是个int,编译器要怎么通过int推导出T呢?这是行不通的。

你可能希望编译器能够通过隐式转换将2转换成Rational,然后编译器成功推导出T的类型是int。但事实上,编译器不会这样做。在template实参推导过程中,编译器从不将隐类型转换函数纳入考虑。

正确使用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可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。

posted @ 2018-10-01 21:50  _NewMan  阅读(223)  评论(0编辑  收藏  举报