7. 模板与泛型编程
41. 了解隐式接口和编译期多态
- 面向对象编程世界总以显示接口和运行期多态解决问题
- 由于w的类型被声明为Widget,所以w必须支持Widget接口。我们可以在源码中找出这个接口(例如在idget的.h文件中),看看它是什么样子,所以我称此为一个显式接口(explicit interface),也就是它在源码中明确可见。
- 由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态(runtime polymorphism),也就是说将于运行期根据w的动态类型(见条款37)决定究竞调用哪一个函数。
- 纵使你从未用过templates,应该不陌生“运行期多态”和“编译期多态”之间的差异,因为它类似于“哪一个重载函数该被调用”(发生在编译期)和“哪一个virtual函数该被绑定”(发生在运行期)之间的差异。
- 通常显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。
- 隐式接口就完全不同了。它并不基于函数签名式,而是由有效表达式(valid expressions)组成。再次看看doProcessing template一开始的条件:
1 |
|
- T(w的类型)的隐式接口看来好像有这些约束:
- 它必须提供一个名为size的成员函数,该函数返回一个整数值。
- 它必须支持一个operator!=函数,用来比较两个T对象。这里我们假设someNastywidget的类型为T。
- 真要感谢操作符重载(operator overloading)带来的可能性,这两个约束都不需要满足
- classes和templates都支持接口(interfaces)和多态(polymorphism)。
- 对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。
- 对template参数而言,接口是隐式的(implicit).,奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。
42. 了解typename的双重意义
- 当我们声明template类型参数,class和typename的意义完全相同
- C++并不总是把class和typename视为等价。有时候你一定得使用typename,为了解其时机,我们必须先谈谈你可以在template内指涉(refer to).的两种名称
- template内出现的名称如果相依于某个template参数,称之为从属名称(dependent names)。如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称(nested dependent name)。
- int是一个并不倚赖任何template参数的名称。这样的名称是谓非从属名称(non-dependent names).。
- 嵌套从属名称有可能导致解析(parsing)困难。
- 如果解析器在template中遭遇一个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。所以缺省情况下嵌套从属名称不是类型。
- 若要矫正这个形势,我们必须告诉C++说c:const iterator是个类型。只要紧临它之前放置关键字typename即可:
- 一般性规则很简单:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字typename。
- typename只被用来验明嵌套从属类型名称;其他名称不该有它存在。
- “typename必须作为嵌套从属类型名称的前缀词”这一规则的例外是,typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class修饰符.
- 让我们看看最后一个typename例子,那是你将在真实程序中看到的代表性例子。假设我们正在撰写一个function template,它接受一个迭代器,而我们打算为该迭代器指涉的对象做一份local复件(副本)temp。我们可以这么写:
1 |
|
- 标准traits
class(见条款47)的一种运用,相当于说“类型为IterT之对象所指之物的类型",这个语句声明一个local变量(temp),使用IterT对象所指物的相同类型,并将temp初始化为iter所指物。如果IterT是vector
::iterator,temp的类型就是int。如果IterT是list ::iterator,temp的类型就是string。由于std:iterator_traits. :value type是个嵌套从属类型名称(value type被嵌套于iterator traits 之内而IterT是个template参数),所以我们必须在它之前放置typename。 - 声明template参数时,前缀关键字class和typename可互换。
- 请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。
43. 学习处理模板化基类内的名称
- 注意class定义式最前头的"template<>”语法象征这既不是template也不是标准class,而是个特化版的MsgSender template,在template实参是CompanyZ时被使用。这是所谓的模板全特化(total template,specialization):template MsgSender针对类型CompanyZ特化了,而且其特化是全面性的,也就是说一旦类型参数被定义为CompanyZ,再没有其他template参数可供变化。
- 令C++“不进入templatized base classes观察”的行为失效。
- 第一是在base class函数调用动作之前加上 this->
- 第二是使用using声明式。如果你已读过条款33,这个解法应该会令你感到熟悉。条款33描述using声明式如何将“被掩盖的base class名称”带入一个derived class作用域内。我们可以这样写下sendClearMsg:
- 第三个做法是,明白指出被调用的函数位于base class内:
- 但这往往是最不让人满意的一个解法,因为如果被调用的是virtual函数,上述的明确资格修饰(explicit qualification)会关闭“virtual绑定行为”。
- 可在derived class templates内通过"this->"指涉base class templates内的成员名称,或藉由一个明白写出的“base class资格修饰符”完成。
44. 将与参数无关的代码抽离templates
- Templates是节省时间和避免代码重复的一个奇方妙法。
- 如果你不小心,使用templates可能会导致代码膨胀:其二进制码带着重复(或几乎重复)的代码、数据,或两者。其结果有可能源码看起来合身而整齐,但目标码(object code)却不是那么回事。
- 共性与变性分析(commonality and variability analysis)。
- 如果你正在编写某个class,而你明白其中某些部分和另一个class的某些部分相同,你也不会重复这共同的部分。取而代之的是你会把共同部分搬移到新clss去,然后使用继承或复合(见条款32,38,39),令原先的classes取用这共同特性。而原classes的互异部分(变异部分)仍然留在原位置不动。
- 编写templates时,也是做相同的分析,以相同的方式避免重复,但其中有个窍门。在non-template代码中,重复十分明确:你可以“看”到两个函数或两个classes之间有所重复。然而在template代码中,重复是隐晦的:毕竟只存在一份template源码,所以你必须训练自己去感受当template被具现化多次时可能发生的重复。
- Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
- 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
- 因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations)的具现类型(instantiation types)共享实现码。
45. 运用成员函数模板接受所有兼容类型
- 所谓智能指针(Smart pointers)是“行为像指针”的对象,并提供指针没有的机能
- 例如条款13曾经提及std:auto_ptr和trl:shared ptr如何能够被用来在正确时机自动删除heap-based资源。STL容器的迭代器几乎总是智能指针:无疑地你不会奢望使用"+"将一个内置指针从linked list的某个节点移到另一个节点,但这在list:iterators身上办得到。
- 真实指针做得很好的一件事是,支持隐式转换(implicit conversions)。Derived class指针可以隐式转换为base class指针,“指向non-const对象”的指针可以转换为“指向cost对象”…等等。
- 但如果想在用户自定的智能指针中模拟上述转换,稍稍有点麻烦
- Templates和泛型编程(Generic Programming)
- 为SmartPtr写一个构造函数,而是为它写一个构造模板。这样的模板(templates)是所谓member function templates(常简称为nember templates),其作用是为class生成函数:
- 以上代码的意思是,对任何类型T和任何类型U,这里可以根据SmartPtr生成一个SmartPtr
,因为Smartptr 有个构造函数接受个SmartPtr参数。这一类构造函数根据对象u创建对象t(例如根据SmartPtr创建一个SmartPtr ),而u和v的类型是同一个template的不同具现体,有时我们称之为泛化(generalized)copy构造函数。 - 泛化copy构造函数并未被声明为explicit。那是蓄意的,因为原始指针类型之间的转换(例如从derived class指针转为base class指针)是隐式转换,无需明白写出转型动作(cst),所以让智能指针仿效这种行径也属合理。在模板化构造函数(templatized constructor)中略去explicit就是为了这个目的。
- member function templates(成员函数模板)的效用不限于构造函数,它们常扮演的另一个角色是支持赋值操作。
- 从某个shared ptr类型隐式转换至另一个shared ptr类型是被允许的,但从某个内置指针或从其他智能指针类型进行隐式转换则不被认可(如果是显式转换如cast强制转型动作倒是可以)
- 请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。
- 如果你声明member templates用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。
46. 需要类型转换时请为模板定义非成员函数
- 这项技术的一个趣味点是,我们虽然使用friend,却与friend的传统用途“访问class的non-public成分”毫不相千。为了让类型转换可能发生于所有实参身上,我们需要一个non-member函数(条款24);为了令这个函数被自动具现化,我们需要将它声明在class内部;而在class内部声明non-member函数的唯一办法就是:令它成为一个friend。
- 一如条款30所说,定义于class内的函数都暗自成为inline,包括像operator这样的friend函数。你可以将这样的inline声明所带来的冲击最小化,做法是令perator不做任何事情,只调用一个定义于class外部的辅助函数。在本条款的例子中,这样做并没有太大意义,因为operator*已经是个单行函数,但对更复杂的函数而言,那么做也许就有价值。“令friend函数调用辅助函数”的做法的确值得细究一番。
- 当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。
47. 请使用traits classes表现类型信息
- STL主要由“用以表现容器、迭代器和算法”的templates构成,但也覆盖若干工具性templates,其中一个名为advance,.用来将某个迭代器移动某个给定距离:
- random access迭代器支持迭代器算术运算,只耗费常量时间,因此如果面对这种迭代器,我们希望运用其优势。
- 这种做法首先必须判断iter是否为random access迭代器,也就是说需要知道类型IterT是否为random access迭代器分类。换句话说我们需要取得类型的某些信息。那就是traits让你得以进行的事:它们允许你在编译期间取得某些类型信息。
- Traits并不是C++关键字或一个预先定义好的构件;它们是一种技术,也是一个C++程序员共同遵守的协议。这个技术的要求之一是,它对内置(built-in)类型和用户自定义(user-defined)类型的表现必须一样好。举个例子,如果上述advance收到的实参是一个指针(例如const char*)和一个int,上述advance仍然必须有效运作,那意味traits技术必须也能够施行于内置类型如指针身上。
- “traits必须能够施行于内置类型”意味“类型内的嵌套信息(nesting information)”这种东西出局了,因为我们无法将信息嵌套于原始指针内。因此类型的traits信息必须位于类型自身之外。标准技术是把它放进一个template及其或多个特化版本中。这样的templates在标准程序库中有若干个,其中针对迭代器者被命名为iterator_traits:
- 现在,你应该知道如何设计并实现一个traits class了:
- 确认若干你希望将来可取得的类型相关信息。例如对迭代器而言,我们希望将来可取得其分类(category)。
- 为该信息选择一个名称(例如iterator_category)。
- 提供一个template和一组特化版本(例如稍早说的terator traits),内含你希望支持的类型相关信息。
- 现在我们可以总结如何使用一个traits class了:
- 建立一组重载函数(身份像劳工)或函数模板(例如doAdvance),彼此间的差异只在于各自的t红aits参数。令每个函数实现码与其接受之traits信息相应和。
- 建立一个控制函数(身份像工头)或函数模板(例如advance),它调用上述那些“劳工函数”并传递traits class所提供的信息。
- Traits广泛用于标准程序库。其中当然有上述讨论的iterator traits,.除了供应iterator_category还供应另四份迭代器相关信息(其中最有用的是value type,见条款42)。此外还有char traits用来保存字符类型的相关信息,以及numeric limits用来保存数值类型的相关信息,例如某数值类型可表现之最小值和最大值等等;命名为numeric limits有点让人惊讶,因为traits classes的名称常以"traits”结束,但numeric_limits却没有遵守这种风格。
- TR1(条款54)导入许多新的traits classes用以提供类型信息,包括is
fundamental
(判断T是否为内置类型),is array (判断T是否为数组类型),以及is base of<T1,T2>(T1和T2相同,抑或r1是T2的base class)。总计TR1一共为标准C+添加了50个以上的traits classes。 - Traits classes使得“类型相关信息”在编译期可用。它们以templates和“templates特化”完成实现。
- 整合重载技术(overloading)后,traits classes有可能在编译期对类型执行if...else测试。
48. 认识template元编程
- TMP有两个伟大的效力。
- 第一,它让某些事情更容易。如果没有它,那些事情将是困难的,甚至不可能的。
- 第二,由于template metaprograms执行于C++编译期,因此可将工作从运行期转移到编译期。
- 这导致的一个结果是,某些错误原本通常在运行期才能侦测到,现在可在编译期找出来。另一个结果是,使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。然而将工作从运行期移转至编译期的另一个结果是,编译时间变长了。是的,程序如果使用TMP,其编译时间可能远长于不使用TMP的对应版本。
- 条款47指出,这个typeid-based解法的效率比traits解法低,因为在此方案中,(1)类型测试发生于运行期而非编译期,(2)“运行期类型测试”代码会出现在(或说被连接于)可执行文件中。实际上这个例子正可彰显TMP如何能够比“正常的”C++程序更高效,因为traits解法就是TMP。别忘了,traits引发“编译期发生于类型身上的if.…else计算”。
- TMP已被证明是个“图灵完全”(Turing-complete)机器,意思是它的威力大到足以计算任何事物。使用TMP你可以声明变量、执行循环、编写及调用函数…但这般构件相对于“正常的”C++对应物看起来很是不同,例如条款47展示的TMP if..else条件句是藉由templates和其特化体表现出来。不过那毕竟是汇编语言层级的TMP。针对TMP而设计的程序库(例如Boost's MPL,见条款55)提供更高层级的语法一一尽管目前还不足以让你误以为那是“正常的”C++。
- TMP并没有真正的循环构件,所以循环效果系藉由递归(recursion.)完成。TMP主要是个“函数式语言”(functional language),而递归之于这类语言就像电视之于美国通俗文化一样地无法分割。TMP的递归甚至不是正常种类,因为TMP循环并不涉及递归函数调用,而是涉及“递归模板具现化”(recursive template instantiation)。
- Template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
- TMP可被用来生成“基于政策选择组合”(based on combinations of policy choices.)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。
7. 模板与泛型编程
http://binbo-zappy.github.io/2024/11/27/effective-cpp/7-模板与泛型编程/