栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

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

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

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

模板与泛型编程 条款 41:了解隐式接口和编译期多态

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

templat
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 Widget;
template class Widget;

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

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

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

template
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
void print2nd(const C& container) {
	typename C::const_iterator* x;
	...
}

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

template
class Derived : public Base::Nested {  // base class list 中不允许 typename
public:
	explicit Derived(int x) 
		: Base::Nested(x) {  // 成员初始值列表中不允许 typename
			typename Base::Nested temp;  // 作为一个 base class 修饰符需加上 typename
		}
};
// typedef typename 是指涉嵌套从属类型名称的一个合理附带结果
template
void workWithIterator(IterT iter) {
	typedef typename std::iterator_traits::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
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
class LoggingMsgSender : public MsgSender {
public:
    void sendClearMsg(const MsgInfo& info) {
        // 将“传送前”的信息写至 log
        sendClear(info);  // 调用 base class 函数:这段代码无法通过编译
        // 将“传送后”的信息写至 log
    }
};

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

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

方法一

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

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

第二是使用 using 声明式:

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

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

方法三

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

template
class LoggingMsgSender : public MsgSender {
public:
    void sendClearMsg(const MsgInfo& info) {
        MsgSender::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
class SquareMatrix {
public:
    ...
    void invert();  // 求逆矩阵
};

现在考虑下面操作:

SquareMatrix sml;
sm1.invert();  // 调用 SquareMatrix::invert
SquareMatrix sm2;
sm2.invert();  // 调用 SquareMatrix::invert

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

我们可以令 SquareMatrixBase 贮存一个指针,指向矩阵数值所在的内存。而只要它存储了那些东西,也就可能存储矩阵尺寸。成果看起来像这样:

template
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
class SquareMatrix: private SquareMatrixBase {
public:
    SquareMatrix()  // 送出矩阵大小和数据指针给 base class
    : SquareMatrixBase(n, data) { }
private:
    T data[n * n];
};

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

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

这个条款只讨论由 non-type template parameters(非类型模板参数)带来的膨胀,其实 type parameters(类型参数)也会导致膨胀。例如在许多平台上 int 和 long 有相同的二进制表述,所以像 vector 和 vector 的成员函数有可能完全相同 —— 这正是膨胀的最佳定义。某些连接器会合并完全相同的函数实现码,有些不会,后者意味着某位 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
class SmartPtr {
    public:  // 智能指针通常以内置指针完成初始化
    explicit SmartPtr(T* realPtr);
};
SmartPtr pt1 = SmartPtr(new Middle);
SmartPtr pt2 = SmartPtr(new Bottom);
SmartPtr pct2 = pt1;

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

template 和泛型编程

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

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

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

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

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

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

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

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

member templates 并不改变语言规则

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

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

请记住:

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

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

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

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

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

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

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

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

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

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

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

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

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

template
    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
void advance(Iter& iter, DistT d);

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

template
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
struct iterator_traits;

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

iterator_traits 针对每一个类型的 IterT,在 struct iterator_traits 内一定声明为某个 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
struct iterator_traits {
	typedef typename IterT::iterator_category iterator_category;
};

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

template
struct iterator_traits {
	typedef random_access_iterator_tag iterator_category;
};

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

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

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

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

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

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

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

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

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

template
void advance(Iter& iter, DistT d) {
	doAdvance(iter, d, 
		typename std::iterator_traits::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
void advance(Iter& iter, DistT d) {
	if (typeid(typename std::iteraot_traits::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::iterator iter;
...
advance(iter, 10);  // 移动 iter 向前走 10 个元素;上述实现无法通过编译

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

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

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

template
struct Factorial {  // 一般情况
	enum { value = n * Factorial::value };
};

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

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

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

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

请记住:

  • TMP 可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
  • TMP 可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。
转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号