文章目录
七、模板与泛型编程
C++ template 机制自身是一部完整的图灵机(Turing-complete):它可以被用来计算任何可计算的值。于是导出了模板元(template metaprogramming),创造出“在C++编译器内执行并于编译完成时停止执行”的程序。
41. 隐式接口和编译器多态
Understand implicit interfaces and compile-time polymorphism.
- classes 和 templates 都支持接口(interfaces)和多态(polymorphism);
- classes 的接口是显式的(explicit),以函数签名为中心(函数签名由函数名称、参数类型、返回类型(对于template function而言)构成,详细见:C++中的函数签名),且在编译期完成代码检查。classes 的多态是通过 virtual 函数对 pointer / reference 的动态绑定实现的,即是运行期多态(runtime polymorphism),详见:C++的动态绑定和静态绑定;
- 对 template 的参数而言,接口是隐式的(implicit),是由有效表达式(valid expressions)组成(什么是有效表达式?),也是在编译期完成代码检查。template 的多态是通过 template 具现化和函数重载解析(function overloading resolution),称为编译期多态(compile-time polymorphism);
42. typename 的双重意义(只出现在模板和泛型编程当中)
Understand the two meanings of typename.
- 该条款可见:typename关键字;
- template 内出现的名称如果相依于某个 template 参数,称之为 从属名称(dependent names),相反地,若是不依赖任何 template 的名称,称之为 非从属名称(independent names)。如果从属名称嵌套在某个 class 内,称其为 嵌套从属名称(nested dependent names);
- 在类作用域概念中,在类外部访问类中的名称时,可以使用类作用域操作符
::
,形如MyClass::name的调用通常存在三种:①静态数据成员、②静态成员函数和③嵌套类型。在使用C::const_iterator* x
时,若不指明其为嵌套从属名称,c++编译器会将其当作静态成员变量而施行乘法运算,而不是变量声明式,为消除该歧义引入了 typename 关键字; - 声明 template 参数时,前缀关键字 class 和 typename 可以互换。 即
template <typename T>
和template <class T>
是等价的; - 在模板和泛型编程中,使用关键字 typename 标示嵌套从属类型,否则编译器将其视作静态成员 ;
- typename 不得在 base class lists(基类列,即
: public/protected/private base_class
)或 member initialization list(成员初值列,即初始化列表)内使用其修饰 base class ;
43. 派生类访问其模板基类内的名称
Know how to access names in templatized base classes.
- 当 base classes 从 templates 中被具现(instantiated)化时, c++ 编译器假设:对 template base classes 的特化版本可能不会提供和一般性 template 相同的接口,所以编译器在派生类实现时对于其所继承的模板基类中的名称之间无视,如若不加干预,会导致派生类无法访问到其所继承的模板基类内的名称;
- 模块化基类(templatized base classes)内的函数会被其模块化派生类(templatized derived class)掩盖;
- 针对上条提出的c++"不进入 templatized base classes观察"所导致的行为失效,有如下三种解决方式:
①在 base class 函数调用动作之前加上this->
;
②使用using
声明式,和 条款33:避免遮掩继承而来的名称中的用法,用法相同,但是这里解决的是编译器不进入模板基类中查看基类成员的问题,而不是继承后基类名称被派生类所遮掩的问题;
③明白指出调用的函数位于 base class内;
44. 将与参数无关的代码从template中剥离
Factor parameter-independent code out of templates.
- 共性和变性分析(commonality and variability analysis):即是将重复的代码剥离出来作为一个新的函数或类型,再在原有的会造成重复代码的函数或类型中调用或继承这一新的函数或类型。在 template 代码中,重复是隐晦的,毕竟只存在一份 template 源码;
- Template 生成多个 classes 和多个函数,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系;
- 因 非类型模板参数(non-type template parameters) 而造成的代码膨胀,往往可以消除,做法是以函数参数或 class 成员变量替代 template 参数;
- 因 类型参数(type parameters) 而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations) 的具现类型(instantiation types)共享实现码;
45. 使用成员模板函数实现所有兼容类型的转换
Use member function templates to accept “all compatible types”.
- naive pointer 可以做到支持安全地进行隐式类型转换(implicit conversion):derived class ptr 隐式向上转换为 base class ptr;指向 non-const object ptr 可以转换为 const object ptr;…;
- template smart pointer 若想做到和 naive pointer 一样的功能,需要在 template 实现中运用 copy template ctor(member function templates(常简称为member templates),作用是为 class 生成函数),又称为泛化(generalized) copy 构造函数,同时因为支持隐式转换功能,所以不能够由 explicit 关键字修饰;
- 在 std::auto_ptr 和 tr1::shared_ptr 中,均提供一个 get 成员函数,返回智能指针对象所持有的原始指针的副本(见条款13,条款15)。利用该实现方式,在 copy template ctor 的针对 heldPtr(所持有原始指针)的初始化列表中使用 get() 成员函数,以实现从可兼容类型进行隐式转换目的(即将智能指针的兼容类型转换转化为所持有原始指针的类型转换);
- smart pointer 的模板参数一般指的是 class,所以不论是 smart pointer class 还是其内含的 template function member 的模板参数声明使用 class 要比 typename 更加直观;
- 如果声明了 member templates 用于 泛化copy构造 或 泛化assignment操作,还是需要对编译器(自动)/自定义生成的正常 copy构造函数 和 copy assignment操作符 进行声明;
46. 需要类型转换时为模板定义非成员函数
Define non-member functions inside templates when type conversions are desired.
- 在条款24中,我们知道了在不涉及泛型和模板编程的前提下,对于需要进行隐式转换的运算函数,将其声明且定义为 non-member 函数最佳,见:条款24. 若所有参数皆需类型转换,请为此采用 non-member 函数;
- template 实参推导过程中并不考虑采纳“通过构造函数而发生的”隐式类型转换,所以对于条款24中的non-member函数实现中的实参进行隐式构造转换再进行
Rational::operator*()
运算的方法不再行得通,编译器不会通过,因为找不到相符的函数; - 对于在上条中在泛型和模板编程中参数存在隐式转换情形下出现的问题,其本质是,对于编译器而言,在调用一个函数之前,必须知道这个函数的存在,而 template 实参推导过程中不考虑隐式类型转换函数,所以编译器不会对这个隐式转型函数进行 template 推导,导致编译器找不到该函数,进而报错;
- template 实参推导只应用于 function templates 身上,而不应用于 class templates,所以编译器总是能在 class templates 具现化是 知道模板参数T的类型;
- 所以以上所提问题的解决方法为:将原本的 non-member 函数声明为模板类的 friend fucntion(友元函数),同时为保证编译通过后能够正常连接,将友元函数的定义置于类定义区块内(作为inline函数),或基于对代码膨胀的控制,调用一 non-member 辅助函数;
- 在一个 class template 内, template 名称可以用来作为"template 和其参数"的简约表达方式,例如:在
Rational<T>
定义区块内,Rational
等价于Rational<T>
; - 当编写一个 class template,而它所提供之"与此 template相关的"函数支持"所有参数之隐式类型转换"时,将这些函数定义为 class template 内部的 friend 函数;
47. 使用 traits classes 表现类型信息 : 以iterator_traits为例
Use traits classes for information about types.
- 对于iterator_traits机制的实现和相关细节,还有迭代器的5种分类及它们的间的继承关系见:STL迭代器详解;
- traits 并不是 c++ 关键字或一个预先定义好的构件,而是一种技术,一个 c++ 程序员共同遵守的协议。它对内置(built-in)类型和用户自定义(user-defined)类型的表现必须一样好。
- traits 必须能够施行在内置类型意味着类型内的嵌套信息无法使用,对于 naive pointer 无法添加类内嵌套信息,所以 traits 信息必须置于类型之外。标准技术将其放进一个 template 及其一个或多个特化版本中,实现时,iterator_traits 是个 struct,习惯上 traits 总是被实现为 structs,但往往称为 traits classes;
- iterator_traits 的功能运作通常分为两部分:
① 在每一个“用户自定义的迭代器类型”(self-defined type’s iterator)必须嵌套一个typedef,名为iterator_category,用以确认适当的卷标结构(tag struct);
② 对于 iterator_traits萃取机,仅作萃取/提取iterator当中的嵌套式 typedef,针对 naive pointer 不能嵌套 typedef 的情况编写偏特化版本的iterator_traits即可; - 在对 forward iterator 的实现中,可以使用 input iterator 的实现,因为 public 继承的 is-a relationship 的缘故,所实现的 input iterator 的实现可以套用到 forward iterator 上去,见 条款32:public 继承 = “是一个” (is-a);
- 如何使用/实现一个 traits class:
① 建立一组重载函数或模板函数(可理解为包身工,也类似于NVI中的imlp,就是底层执行动作的辅助函数),彼此间的差异仅在于各自的 traits 参数,令每个函数实现码与其接受之 traits 信息保持一致;
② 建立一个控制函数(可理解为地主?也类似于NVI中提供函数接口并调用virtual声明的imlp的非virtual接口函数),用它调用上条中的”包身工“函数并萃取出traits/传递traits class所提供的信息; - 在 std 程序库中,除了 iterator_traits,还有 char_traits 用来保存字符类型的相关信息,以及 numeric_limits 用来保存数值类型的相关信息;
- traits classes 使得相关类型信息在编译期可用,以 template 和 template 特化技巧实现;
- 整合重载技术后,traits classes 有可能在编译器对类型执行 if-else 测试;
48. 认识 template 元编程
Be aware of template metaprogramming.
- Template metaprogramming(TMP,模板元编程)是编写 template-based C++ 程序并执行于编译器的过程。Template metaprogram(模板元程序)即是以 C++ 写成,执行于 C++ 编译器内的程序,一旦TMP程序结束执行,也就是从 templates 具现出的若干 C++ 源码,便会一如往常地被编译;
- 非TMP编程,编译器在编译过程中必须确保所有源码都有效,纵使不会执行的代码;而traits-based的TMP解法,针对不同类型而进行的代码,会拆分为不同的函数,每个函数所使用的操作(操作符)都可施行于该函数所对付的类型;
- TMP没有真正的循环构件,其循环效果由递归(recursion)完成,其主要是个“函数式语言”(functional language),其递归也不是正常种类,因为TMP循环不涉及递归函数调用,而是涉及“递归模板具现化(recursive template instantistion)”;
- TMP的起手程序是在编译期计算阶乘(factorial);
- TMP可将工作由运行期移往编译器,因而得以实现早期错误侦测和更高的执行效率;
- TMP可被用来生成基于政策选择组合(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些类型特殊类型并不适合的代码;