0
点赞
收藏
分享

微信扫一扫

Effective C++ 07 模板与泛型编程

鱼板番茄 2022-05-02 阅读 64
c++

模板与泛型编程

条款 41:了解隐式接口和编译期多态

面向对象编程总是以显式接口和运行期多态解决问题。template 及泛型编程,与面向对象有本质上的不同。在此世界中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口和编译期多态移到前面了。参考下面例子:

templat<typename T>
void doProcessing(T& w) {
	if (w.size() > 10 && w != someNastyWidget) {
		T temp(w);
		temp.normalize();
		temp.swap(s);
	}
}
  • w 必须支持哪一种接口,是由 template 中执行于 w 身上的操作来决定。就本例来看 w 的类型 T 好像必须支持 size、normalize 和 swap 成员函数、copy 构造函数、不等式比较,这一组表达式便是 T 必须支持的一组隐式接口。
  • 凡涉及 w 的任何函数调用,例如 operator> 和 operator!=,有可能造成 template 具现化,使这些调用得以成功。这样的具现行为发生在编译期。以不同的 template 参数具现化 function template 会导致调用不同的函数,这边是所谓编译器多态。
显式接口与隐式接口

通常显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。例如:

class Widget {
public:
	Widget();
	virtual ~Widget();
	virtual std::size_t size() const;
	virtual void normalize();
	void swap(Widget& other);
};

其 public 接口由一个构造函数、一个析构函数、函数 size、normalize、swap 及其参数类型、返回类型、常量性构成。当然也包括编译器产生的 copy 构造函数和 copy assignment 操作符。另外也可以包括 typedef,以及 public 变量成员。

隐式接口就完全不同了。它并不基于函数签名式,而是由有效表达式组成。加诸于 template 参数身上的隐式接口,就像加诸于 class 对象身上的显式接口一样真实,而且二者都是在编译期完成检查。就像你无法以一种“与 class 提供的显式接口矛盾”的方式来使用对象(代码将无法通过编译),你也无法在 template 中使用“不支持 template 所要求的隐式接口”的对象(代码一样无法通过编译)。

请记住:

  • class 和 template 都支持接口和多态。
  • 对 class 而言接口时显式,以函数签名为中心。多态则是用过 virtual 函数发生于运行期。
  • 对 template 参数而言,接口是隐式,奠基于有效表达式。多态则是通过 template 具现化和函数重载解析发生于编译期。

条款 42:了解 typename 的双重意义

当我们声明 template 类型参数时,class 和 typename 的意义完全相同,即下面二者完全相同:

template<class T> class Widget;
template<typename T> class Widget;

然而 C++ 并不总是把 class 和 typename 是为等价。有时候你一定得使用 typename。

template 内出现的名称如果相依于某个 template 参数,称其为从属名称。如果从属名称在 class 内成嵌套状,我们称它为嵌套从属名称。类似 T::const_iterator,实际上它还是个嵌套从属类型名称,也就是个嵌套从属名称并且指涉某类型。

嵌套从属名称可能会导致解析困难,举个例子:

template<typename C>
void print2nd(const C& container) {
	C::const_iterator* x;
	...
}

看起来好像我们声明 x 为一个 local 变量,它是个指针,指向一个 C::const_iterator。但是如果 C::const_iterator 不是个类型呢?如果 C 有个 static 变量名称恰好被命名为 const_iterator,或如果 x 碰巧是一个 global 变量名称,那样的话上述代码就不是声明一个 local 变量,而是也给相乘动作: C::const_iterator 乘以 x。

C++ 有个规则可以解析此歧义状态:如果解析器在 template 中遭遇一个嵌套从属名称,它便假设者名称不是个类型,除非你告诉它是。所以缺省情况下嵌套从属名称不是类型。如果要告诉 C++ 说 C::const_iterator 是一个类型,只需要在它之前添加关键字 typename 即可:

template<typename C>
void print2nd(const C& container) {
	typename C::const_iterator* x;
	...
}

一般性规则是:任何时候当你想要在 template 中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字 typename,但是有一个例外:typename 不可以出现在 base class list 内的嵌套从属类型名称之前,也不可在 member initialization list(成员初始值列表)中作为 base class 修饰符。例如:

template<typename T>
class Derived : public Base<T>::Nested {  // base class list 中不允许 typename
public:
	explicit Derived(int x) 
		: Base<T>::Nested(x) {  // 成员初始值列表中不允许 typename
			typename Base<T>::Nested temp;  // 作为一个 base class 修饰符需加上 typename
		}
};
// typedef typename 是指涉嵌套从属类型名称的一个合理附带结果
template<typename IterT>
void workWithIterator(IterT iter) {
	typedef typename std::iterator_traits<IterT>::value_type vlue_type;
	value_type temp(*iter);
}

请记住:

  • 声明 template 参数时,前缀关键字 class 和 template 可互换。
  • 请使用关键字 typename 标识嵌套丛书类型;但不得在 base class list(基类列表)或 member initialization list(成员初始化列表)内以它作为 base class 修饰符。

条款 43:学习处理模板化基类内的名称

假设我们需要撰写一个程序,它能够传送信息到若干不同的公司去。信息要不译成密码,要不就是未经加工的文字。如果编译期间我们有足够信息来决定哪一个信息传至哪一家公司,就可以采用基于 template 的解法:

class CompanyA {
public:
    void sendCleartext(const std::string& msg);
    void sendEncrypted(const std::string& msg);
};
class CompanyB {
public:
    void sendCleartext(const std::string& msg);
    void sendEncrypted(const std::string& msg);
};
class MsgInfo { ... };  // 这个 class 用来保存信息,以备将来产生信息
template<typename Company>
class MsgSender {
public:
    ...  // 构造函数、析构函数等等
    void sendClear(const MsgInfo& info) {
        std::string msg;
        ... // 根据 info 产生信息
        Company c;
        c.sendCleartext(msg);
    }
    // 类似 sendClear,唯一不同是,这里调用 c.sendEncrypted
    void sendSecret(const MsgInfo& info) { ... }  
};

此时,假设我们有时候想要在每次送出信息时 log 某些信息。derived class 可轻易加上这样的生产力,那似乎是个合情合理的解法:

template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
    void sendClearMsg(const MsgInfo& info) {
        // 将“传送前”的信息写至 log
        sendClear(info);  // 调用 base class 函数:这段代码无法通过编译
        // 将“传送后”的信息写至 log
    }
};

上述代码无法通过的原因在于,当编译器遭遇 class template LoggingMsgSender 定义式时,并不知道它继承什么样的 class。当然它继承的是 MsgSender<Company>,但其中的 Company 是个 template 参数,无法确切知道它是什么(直到 LoggingMsgSender 被具现化)。而如果不知道 Company 是什么,就无法知道 class MsgSender<Company> 是否有 sendClear 函数(因为 base class template 模板有可能被特例化)。

我们有三种方法解决上述问题。

方法一

第一是在 base class 函数调用动作之前加上 “this->”:

template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
    void sendClearMsg(const MsgInfo& info) {
        // 将“传送前”的信息写至 log
        this->sendClear(info);  // 成立,假设 sendClear 将被继承。
        // 将“传送后”的信息写至 log
    }
};
方法二

第二是使用 using 声明式:

template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
    using MsgSender<Company>::sendClear;  // 告诉编译器,请它假设 sendClear 位于 base class 内
    void sendClearMsg(const MsgInfo& info) {
        sendClear(info);  // 成立,假设 sendClear 将被继承
    }
};

这里的情况并不是 base class 名称被 derived class 名称覆盖,而是编译器不进入 base class 作用域内查找,于是我们通过 using 声明告诉它,请它这么做。

方法三

第三个做法是,明确指出被调用的函数位于 base class 内:

template<typename Company>
class LoggingMsgSender : public MsgSender<Company> {
public:
    void sendClearMsg(const MsgInfo& info) {
        MsgSender<Company>::sendClear(info);  // 成立,假设 sendClear 将被继承。
    }
};

这往往是最不让人满意的一个解法,因为如果被调用的是 virtual 函数,上述的明确资格修饰会关闭 virtual 绑定行为。

从名称可视点的角度出发,上述每一个做法都做的事情都相同:对编译器承诺 base class template 的任何特化版本都将支持一般(泛化)版本所提供的接口。

请记住:

  • 可在 derived class templates 内通过 “this->” 指涉 base class templates 内的成员名称,或籍由一个明确写出的 “base class 资格修饰符” 完成。

条款44:将与参数无关的代码抽离 template

template 是节省时间和避免代码重复的一个奇方妙法。不再需要键入多个类似的 classe,你只需键入一个 class template,留给编译器去具现化剩余你需要的相关 classe 和函数。class template 的成员函数只有在被使用时才被暗中具现化,只有在每一个函数都被使用时,你才会获得所有的函数。

有时候,使用 template 可能会导致代码膨胀:其二进制码带着重复(或几乎重复)的代码、数据。通过共性与变形分析,可以尽可能减轻。例如,你会抽出两个函数的共同部分,把它们放进第三个函数中,然后令原先两个函数调用这个新函数。编写 template 时,也是做相同的分析,以相同的方式避免重复,但其中有个窍门。在 non-template代码中,重复十分明确:你可以“看”到两个函数或两个 classe 之间有所重复。然而在 template 代码中,重复是隐晦的。参考下面例子:

template<typename T, std::size_t n>
class SquareMatrix {
public:
    ...
    void invert();  // 求逆矩阵
};

现在考虑下面操作:

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

这次具现化两份 invert。这些函数并非完完全全相同,但除了常量 5 和 10,两个函数的其他部分完全相同。这是 template 引出代码膨胀的一个典型例子。

我们可以令 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;  // 指针,指向矩阵内容
};

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

template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
    SquareMatrix()  // 送出矩阵大小和数据指针给 base class
    : SquareMatrixBase<T>(n, data) { }
private:
    T data[n * n];
};

不论数据存储于何处,从膨胀角度讨论,SquareMatrix 成员函数可以单纯地以 inline 方式调用 base class 版本,后者持有同型元素(不论矩阵大小)的所有矩阵共享。SquareMatrix<double, 5> 和 SquareMatrix<double, 10> 对象使用相同的 SquareMatrixBase<double> 成员函数。

从另一个角度看,不同大小的矩阵中只拥有单一版本 invert,可减少执行文件大小,也就因此降低程序的 working set 大小,并强化指令高速缓冲区内的引用集中化。这些都可能是程序执行的更快速。
注: working set 是指对一个在虚内存环境下执行的进程而言,其所使用的那一组内存页。

这个条款只讨论由 non-type template parameters(非类型模板参数)带来的膨胀,其实 type parameters(类型参数)也会导致膨胀。例如在许多平台上 int 和 long 有相同的二进制表述,所以像 vector<int> 和 vector<long> 的成员函数有可能完全相同 —— 这正是膨胀的最佳定义。某些连接器会合并完全相同的函数实现码,有些不会,后者意味着某位 template 被具现化为 int 和 long 两个版本,并因此造成代码膨胀。

请记住:

  • templates 生成多个 class 和多个函数,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系。
  • 因非类型模板参数而造成的代码膨胀,往往可以消除,做法是以函数参数或 class 成员变量替换 template 参数。
  • 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述的具现类型共享实现码。

条款 45:运用成员函数模板接受所有兼容类型

真实指针做得很好的一件事是,支持隐式转换。derived class 指针可以隐式转换为 base class 指针,“指向 non-const 对象”的指针可以转换为“指向 const 对象” 等等。下面是可能发生于三层继承体系的一些转换:

class Top { ... };
class Middle: public Top { ... };
class Bottom: public Middle { ... };
Top* pt1 = new Middle;  // 将 Middle* 转换为 Top*
Top* pt2 = new Bottom;  // 将 Bottom* 转换为 Top*
const Top* pct2 = pt1;  // 将 Top* 转换为 const Top*

但如果想在用户自定的智能指针中模拟上述转换,稍稍有点麻烦。我们希望以下代码通过编译:

template<typename T>
class SmartPtr {
    public:  // 智能指针通常以内置指针完成初始化
    explicit SmartPtr(T* realPtr);
};
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top> pct2 = pt1;

但是,同一个 template 的不同具现体之间并不存在什么与生俱来的固有关系,所以编译器视 SmartPtr<Middle> 和 SmartPtr<Top> 为完全不同的 class。

template 和泛型编程

就原理而言,此例中我们需要的构造函数数量没有止尽,因为一个 template 可被无限量具现化,已致生成无限量函数。因此,似乎我们需要的不是为 SmartPtr 写一个构造函数,而是为它写一个构造模板。这样的模板是所谓 member function templates,其作用是为 class 生成函数:

template<typename T>
class SmartPtr {
public:
    template<typename U>  // member template
    SmartPtr(const SmartPtr<U>& other);  // 未来生成 copy 构造函数
};

以上代码的意思是,对任何类型 T 和任何类型 U,这里可以根据 SmartPtr<U> 生成一个 SmartPtr<T> —— 因为 SmartPtr<T> 有个构造函数接受一个 SmartPtr<U> 参数。这被我们称作泛化 copy 构造函数

上面的泛化 copy 构造函数并未被声明为 explicit。那是蓄意的,因为原始指针类型之间的转换(例如从 derived class 指针转为 base class 指针)是隐式转换,无需明白写出转型动作,所以让智能指针效仿这种行径也属合理。在模板化构造函数中略去 explicit 就是为了这个目的。

完成声明之后,这个为 SmartPtr 而写的“泛化 copy 构造函数”提供的东西比我们需要的更多。比如说,我们不希望根据一个 SmartPtr<double> 创建一个 SmartPtr<int>。是的,我们必须从某方面对这一 member template 所创建的成员函数群进行筛除,使它符合我们的期望:

template<typename T>
class SmartPtr {
public:
    template<typename U>
    SmartPtr(const SmartPtr<U>& other)  // 以 other 的 heldPtr 初始化 this 的 heldPtr
    : heldPtr(other.get()) { ... }
    T* get() const { return heldPtr; }
private:
    T* heldPtr;  // 这个 SmartPtr 持有内置指针
};

我使用成员初值列来初始化 SmartPtr<T> 之内类型为 T* 的成员变量,并以类型为 U* 的指针(由 SmartPtr<U> 持有)作为初值。这个行为只有当“存在某个隐式转换可将一个 U* 指针转为一个 T* 指针”时才能通过编译,而那正是我们想要的。最终效益是 SmartPtr<T> 现在有了一个泛化 copy 构造函数,这个构造函数只在其所获得的实参隶属适当(兼容)类型时才通过编译。

member function template 的效用不限于构造函数,它们常扮演的另一个角色是支持赋值操作。

member templates 并不改变语言规则

member templates 并不改变语言规则,而语言规则说,如果程序需要一个 copy 构造函数,你却没有声明它,编译器会为你暗自生成一个。在 class 内声明泛化 copy 构造函数并不会阻止编译器生成它们自己的 copy 构造函数,所以如果你想要控制 copy 构造函数的方方面面,你必须同时声明泛化 copy 构造函数和“正常的” copy 构造函数。相同规则也适用于赋值操作。下面是 tr1::shared_ptr 的一份定义摘要,例证上述所言:

template<class T>
class shared_ptr {
public:
    shared_ptr(shared_ptr const& r);  // copy 构造函数
    template <class Y>
    shared_ptr(shared_ptr<Y> const& r);  // 泛化 copy 构造函数
                
    shared_ptr& operator=(shared_ptr<Y> const& r);  // copy assignment
    template <class Y>
    shared_ptr& operator=(shared_ptr<Y> const& );  // 泛化 copy assignment
};

请记住:

  • 请使用 member function template(成员函数模板)生成“可接受所有兼容类型”的函数。
  • 如果你声明 member template 用于“泛化 copy 构造”或“泛化 assignment 操作”你还是需要声明正常的 copy 构造函数和 copy assignment 操作符。

条款 46:需要类型转换时请为模板定义非成员函数

条款 24 讨论过为什么唯有 non-member 函数才有能力“在所有实参身上实施隐式类型转换”,本条款首先以一个看似无害的改动扩充条款 24 的讨论:本条款将 Rational 和 operator* 模板化了:

template<typename T>
    class Rational {
    public:
        Rational(const T& numberator = 0, 
	        const T& denominator = 1);
        const T numerator() const;
        const T denominator() const;
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) { ... }

我们希望支持混合式算数运算,所以我们希望以下代码顺利通过编译。与条款 24 所列的同一份代码,唯一不同的是 Rational 和 operator* 如今都成了 templates:

Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2;  // 错误!无法通过编译

这里编译器不知道我们想要调用哪个函数。取而代之的是,它们试图想出什么函数被名为 operator* 的 template 具现化出来。它们知道它们应该可以具现化某个“名为 operator* 并接受两个 Rational<T> 参数”的函数,但为完成这一具现化行动,必须先算出 T 是什么。问题是它们没这个能耐。因为 template 实参推导过程中从不将隐式类型转换函数纳入考虑。

解决上述问题,我们只需要在 template class 内用 friend 声明式指定某个特定函数。这意味 Rational<T> 可以声明 operator* 是它的一个 friend 函数。class template 并不依赖 template 实参推导(后者只施行于 function template 身上),所以编译器总是能够在 class Rational<T> 具现化时得知 T。因此,令 Rational<T> class 声明适当的 operator* 为其 friend 函数,可简化整个问题:

template<typename T>
    class Rational {
    public:
	    // 声明
        friend
        const Rational operator*(const Rational& lhs, const Rational& rhs); 
};
// 定义
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) { ... }

这样对 operator* 的混合式调用就可以通过编译了,因为当对象 oneHalf 被声明为 Rational<int>,class Rational<int> 就被具现化出来了,而作为过程的一部分,friend 函数 operator* 也就被自动声明出来。后者身为一个函数而非函数模板,因此编译器可在调用它时使用隐式转换函数,而这便是混合式调用成功的原因。

在一个 class template 内,template 名称可被用来作为“ template 和其参数”的简略表达方式,所以在 Rational<T> 内我们可以只写 Rational 而不必写 Rational<T>。

混合式代码通过了编译,因为编译器知道我们要调用哪个函数,但那个函数只被声明于 Rational 内,并没有被定义出来。我们意图令此 class 外部的 operator* template 提供定义式,但是行不通 —— 如果我们自己声明了一个函数,就有责任定义那个函数。既然我们没有提供定义式,连接器当然找不到它!

或许最简单的可行办法就是将 operator* 函数本体合并至其声明式内:

template<typename T>
    class Rational {
    public:
        friend
        const Rational operator*(const Rational& lhs, const Rational& rhs) {
            return Rational(lhs.numberator() * rhis.numberator(), lhs.denominator() * rhs.denominator());
        }
};

这项计数的一个趣味点时,我们虽然使用 friend,却与 friend 的传统用途“访问 class 的 non-public 成分”毫不相干。为了让类型转换可能发生于所有实参身上,我们需要一个 non-member 函数(见条款 24);为了零这个函数被自动具现化,我们需要将它声明在 class 内部;而在 class 内部声明 non-public 函数的唯一办法就是,令它成为一个 friend。

请记住:

  • 当我们编写一个 class template,而它所提供之“与此 template 相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“ class template 内部的 friend 函数”。

条款 47:请使用 trait class 表现类型信息

STL 主要由用以表现容器、迭代器和算法的 template 构成,也覆盖若干工具性 template,其中一个名为 advance,用来将某个迭代器移动某个给定距离:

template<typename IterT, typename DistT>
void advance(Iter& iter, DistT d);

观念上 advanc 只是做 iter += d 动作,但其实只有 random access 迭代器才支持 += 操作。其他的迭代器,advance 只能反复执行 ++ 或 – 操作。我们真正希望的是以这种方式来实现 advance:

template<typename IterT, typename DistT>
void advance(Iter& iter, DistT d) {
	if (iter is a random access iteraotr) {
		iter += d;  // 针对 random access 迭代器使用迭代器算术运算
	}
	else {
		if (d >= 0) {
			while (d--) ++iter;
		}
		else {
			while (d++) --iter;
		}
	}
}

这种做法我们需要知道 IterT 是否为 random access 迭代器分类。换句话说我们需要取得类型的某些信息。那就是 trait 让你得以进行的事:它们允许你在编译期间去的某些类型信息。

trait 并不是 C++ 关键字或一个预先定义好的组件;它们是一种技术,也是一个 C++ 程序员共同遵守的协议。这个技术的要求之一是,它对内置类型和用户自定义类型的表现必须一样好。比如说,不论上述 advance 收到的实参是指针还是一个 int,它都可以有效运作。这意味 trait 计数必须也能够施行与内置类型身上,也就说明类型内的嵌套信息这种东西出局了,因此类型的 trait 信息必须位于类型自身之外。标准技术是把它放进一个 template 及其一个或多个特化版本中。

设计 trait class

针对迭代器的 trait 被命名为 iterator_traits:

template<typename IterT>
struct iterator_traits;

iterator_traits 是个 struct,习惯上 trait 总是被实现为 struct,但它们又往往被称为 trait class

iterator_traits 针对每一个类型的 IterT,在 struct iterator_traits<IterT> 内一定声明为某个 typedef 名为 iterator_category。这个 typedef 用来确认 IterT 的迭代器分类。例如,deque 的迭代器可随机访问,所以一个针对 deque 迭代器而设计的 class 看起来会是这样子:

template <...>  // 省略
class deque {
public:
	class iterator {
	public:
		typedef random_access_iterator_tag iterator_category;
	};
};

iterator_traits,只是鹦鹉学舌般相应 iterator class 的嵌套式 type:

template<typename IterT>
struct iterator_traits {
	typedef typename IterT::iterator_category iterator_category;
};

这可以支持用户自定义类型,但是对指针行不通,因为指针不可能嵌套 typedef。为了支持指针迭代器,iterator_traits 特别针对指针类型提供一个偏特化版本:

template<typename IterT>
struct iterator_traits<IterT*> {
	typedef random_access_iterator_tag iterator_category;
};

现在,你应该知道如何设计并实现一个 trait class 了:

  • 确认若干你希望将来可取得的类型相关信息。例如对迭代器而言,我们希望将来可取得其分类。
  • 为该信息选择一个名称(例如 iterator_traits)。
  • 提供一个 template 和 一组特化版本,内含你希望支持的类型相关信息。
使用 trait class

现在有了 iterator_traits,我们可以对 advance 实践先前的伪码:

template<typename IterT, typename DistT>
void advance(Iter& iter, DistT d) {
	if (typeid(typename std::iteraot_traits<IterT>::iterator_category) == typeid(std::random_access_iterator_tag))
	...
}

但是上述代码会导致编译问题,IterT 类型在编译期间获知,所以 iteraot_traits<IterT>::iterator_category 也在编译期间确定,但 if 语句却是在运行期才会核定,也就是说将编译器完成的事情拖延到了运行期才执行。这不仅浪费时间,也造成可执行文件膨胀。

我们需要一个针对类型而发生的编译器条件语句,这个方法就是:重载。为了让 advance 的行为如我们所期望,我们需要做的是产生不同的重载函数,内涵 advance 的本质内容,但各自接受不同类型的 iterator_category 对象:

template<typename IterT, typename DistT>
void doAdvance(Iter& iter, DistT d,  // 这份实现用于 random access 迭代器
	std::random_access_iterator_tag) {
		iter += d;
	}

template<typename IterT, typename DistT>
void doAdvance(Iter& iter, DistT d,  // 这份实现用于 bidirectional 迭代器
	std::bidirectional_iterator_tag) {
		if (d >= 0) {
			while (d--) ++iter;
		}
		else {
			while (d++) --iter;
		}
	}
...

有了这些 doAdvance 重载版本,advance 需要做的只是调用它们并额外传递一个对象,后者必须带有适当的迭代器分类:

template<typename IterT, typename DistT>
void advance(Iter& iter, DistT d) {
	doAdvance(iter, d, 
		typename std::iterator_traits<IterT>::iterator_category()
	);
}

现在我们可以总结如何使用一个 traits class 了:

  • 建立一组重载函数(身份像劳工)或模板函数(如 doAdvance),彼此之间差异只在于各自的 trait 参数。令每个函数实现码与其接受之 trait 信息相应和。
  • 建立一个控制函数(身份像工头)或函数模板,它调用上述重载函数(劳工函数)并传递 trait class 所提供的信息。

请记住:

  • trait classe 使得“类型相关信息”在编译期可用。它们以 template 和 “templates 特化”完成实现。
  • 整合重载技术后,trait class 有可能在编译期对类型执行 if…else 测试。

条款 48:认识 template 元编程

template metaprogramming(TMP,模板元编程)是编写 template-based C++ 程序并执行于编译期的过程。所谓 TMP 是以 C++ 写成、执行于 C++ 编译器内的程序。一旦 TMP 程序结束执行,其输出,也就是从 template 具现出来的若干 C++ 源码,便会一如往常地被编译。

TMP有两个伟大的效力。第一,它让某些事情更容易。如果没有它,那些事情将是困难的,甚至不可能的;第二。由于 TMP 执行于 C++ 编译期,因此可将工作从运行期转移到编译期。这导致一个结果是,某些错误原本通常在运行期才能检测到,现在可在编译期找出来。另一个结果是,使用 TMP 的 C++ 程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存要求。然而它也会带来另一个令人 感到不愉快的结果是,编译时间变长了。

继续参考下述代码:

template<typename IterT, typename DistT>
void advance(Iter& iter, DistT d) {
	if (typeid(typename std::iteraot_traits<IterT>::iterator_category) == typeid(std::random_access_iterator_tag)) {
		iter += d;  // 针对 random access 迭代器使用迭代器算术运算
	}
	else {
		if (d >= 0) {
			while (d--) ++iter;
		}
		else {
			while (d++) --iter;
		}
	}
}

条款 47 指出,这个 typeid-base 解法的效率比 trait 解法低,因为在此方案中,类型测试发生于运行期而非编译器,运行其类型测试代码会出现在可执行文件中。实际上这个例子正可彰显 TMP 如何能比正常的 C++ 程序更高效,因为 trait 解法就是 TMP。

参考下面的调用:

std::list<int>::iterator iter;
...
advance(iter, 10);  // 移动 iter 向前走 10 个元素;上述实现无法通过编译

问题出在尝试在 list<int>::iterator 身上使用 += 操作符,但 list<int>::iterator 并不支持 += 操作。虽然我们知道并不会执行 += 那行代码,但是编译器必须确保所有源码都有效,即使是不会执行的代码。而当 iter 不是 random access 迭代器时,iter += d 无效。与此对比的是基于 trait 的 TMP 解法,其针对不同类型而进行的代码,被拆分为不同的函数,每个函数所使用的操作符都可施行于该函数所对付的类型。

使用 TMP 你可以声明变量、执行循环、编写及调用函数,但这般构造相对于正常的 C++ 对应物看起来很不同,例如条款 47 展示的 TMP if…else 条件句是藉由 template 和其特化体表现出来的而 TMP 的循环构件,则是藉由递归完成的

下面将展示 TMP 的循环使用:

template<unsigned n>
struct Factorial {  // 一般情况
	enum { value = n * Factorial<n - 1>::value };
};

template<>
struct Factorial<0> {  // 特殊情况
	enum { value = 1 };
};

有了这个就可以获得 n 阶乘积。

为求领悟 TMP 之所以值得学习,很重要的一点是先对它能够达成什么目标有一个较好的理解。下面举出三个例子:

  • 确保量度单位正确。如果使用 TMP,可以确保(在编译期)程序中所有量度单位的组合都正确,不论其计算多么复杂。这也是为什么 TMP 可被用来进行早期错误侦测。
  • 优化矩阵运算。使用 TMP 相关的 template 技术,就有可能消除那些失灵对象并合并循环,于是 TMP 软件使用较少的内存,执行速度又有所提升。
  • 可以生成客户定制模式的代码。

请记住:

  • TMP 可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
  • TMP 可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。
举报

相关推荐

0 条评论