条款 41:了解隐式接口和编译期多态
-
面向对象的世界总是以显示接口和运行期多态解决问题:【class 支持显示接口和运行期多态】
class Widget { public: Widget(); virtual ~Widget(); virtual std::size_t size() const; virtual void normalize(); void swap(Widget& other); ... }; void doProcessing(Widget& w) { if( w.size() > 10 && w != someNastyWidget ) { Widget temp(w); temp.normalize(); temp.swap(w); } } // 由于w的类型被声明为Widget,所以w必须支持Widget的接口 // 由于Widget的某些成员函数是virtual的,所以将在运行期根据w的动态类型决定调用哪个函数 // 即 w对那些函数的调用表现出运行期多态。
-
如果将上述例子中的
doProcessing
函数转变为函数模板:【template 支持隐式接口和编译期多态,编译期多态实现是依赖 template 具现化和函数重载解析】template<typename T> void doProcessing(T& w) { if( w.size() > 10 && w != someNastyWidget ) { T temp(w); temp.normalize(); temp.swap(w); } } // w必须支持哪一种接口,由template中执行于w身上的操作来决定。 // 只要涉及w的任何函数调用,比如 operator > 与 !=,有可能造成template具现化 // 这样的具现行为发生在编译期。
-
以不同的 template 参数具现化 function templates 会导致调用不同的函数,这便是所谓的编译期多态。
- 编译期多态与运行期多态的差异类似于哪一个重载函数应该被调用(发生在编译期)与哪一个 virtual 函数该被绑定(发生在运行期)。
-
通常显式接口由函数的签名式(函数名称、参数类型、返回类型)构成,隐式接口就不同于此,并不基于函数签名式,而是由有效表达式组成。
条款 42:了解 typename 的双重意义
- 当我们声明 template 类型参数,class 和 typename 的意义完全相同。
- template 内出现的名称如果相依于某个 template 参数,称之为 dependent name(从属名称)。
template<typename C> void print2nd(const C& container) { if(container.size() >= 2) { C::const_iterator iter(container.begin()); ++iter; int value = *iter; std::cout<<value; } } // 这里的iter就是从属名称 // template内出现的名称如果相依于某个template参数,称之为 dependent name(从属名称) // 如果从属名称在 class 内呈嵌套状,我们称它为嵌套从属名称 // 而这里的value就是local变量,并不依赖于任何template参数,也叫非从属名称
- 嵌套从属名称可能会导致解析困难。
- 如果解析器在 template 中遭遇一个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。【缺省情况下嵌套从属名称不是类型】
- 意思就是说:想在 template 中指涉一个嵌套从属类型名称,就必须在紧邻它的前一个位置加上关键字 typename。
- 前面说了,嵌套从属类型名称的前缀词是 typename,但也有不可使用的情况:
- typename 不可以出现在 base_class list 内的嵌套从属名称类型名称之前;
- 也不可以在 member initialization list(成员初值列)中作为 base class 修饰符。
- typename 一般和 typedef 共同使用。
条款 43:学会处理模板化基类内的名称
- 假设
B
是一个模板化基类,D
继承了B
,默认情况下,D
中不能调用B
的方法,因为 C++ 往往拒绝在 templatized base classes(即 base 类是模板)内寻找继承而来的名称。- 因为 C++ 知道 base class templates 有可能被特化,而特化版本可能不提供一般性 template 相同的接口。
- 为解决上述问题,有三种方法:【告诉编译器基类中拥有派生类所需函数】
- 第一种方法,可在 derived class templates 内通过 this 指针来调用模板化基类中的方法(相当于明确表示子类将会实现基类的方法)。
template <typename object> class B:public A<object> { public: void func3() { this->func1();//加上this关键字 } };
- 第二种方法是使用 using 声明式。
template <typename object> class B:public A<object> { using A<object>::func1; public: void func3() { this->func1();//加上this关键字 } };
- 第二种方法是使用 using 声明式。
- 第三种方法是通过
::
符号明确指出调用的函数位于基类中,但这种方法最差劲,因为如果是调用的虚函数,将会关闭动态绑定行为。template <typename object> class B:public A<object> { public: void func3() { A<object>::func1(); } };
- 第一种方法,可在 derived class templates 内通过 this 指针来调用模板化基类中的方法(相当于明确表示子类将会实现基类的方法)。
条款 44:将与参数无关的代码抽离 templates
- Template 是节省时间和避免代码重复的一个奇妙的方法。
- class templates 的成员函数只有在被使用时才被暗中具现化。(fuction template 也差不多)
- 使用 templates 可能会导致代码膨胀:其二进制码带着重复的代码、数据或两者都有。
- 为了避免这类错误,需要进行共性与变性分析。
- 对于函数:分析两个函数,找出共同的部分和变化的部分,把共同的部分搬到一个新函数去,保留变化的部分在原函数中不动。
- 对于类:令原先的 classes 取用共同特性,而原 classes 的互异部分仍然在原位置不动。
- 对于 template:
- template 生成多个 class 和多个函数,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系。
- 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或 class 成员变量替换 template 参数。
- 因类型参数(type parameter)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations)的具现类型(instantiation types)共享实现码。
条款 45:运用成员函数模板接受所有兼容类型
- 同一个 template 的不同具现体之间并不存在什么与生俱来的固有关系。
- 即编译器将
SmartPtr<A>
和SmartPtr<B>
看做两个完全不同的类。
- 即编译器将
- 使用 member function templates 生成可接受所有兼容类型的函数:【泛化 copy 构造函数】
template<typename T> class SmartPtr { public: template<typename U> SmartPtr(const SmartPtr<U>& other); // 生成copy构造函数 ... }; // 上述代码是指:对任何类型T和任何类型U,这里可以根据SmartPtr<U>生成一个SmartPtr<T> // 因为 SmartPtr<T> 有个构造函数接受一个SmartPtr<U>参数。
- 我们希望根据一个
SmartPtr<Derived>
创建一个SmartPtr<Base>
,却不希望根据一个SmartPtr<Base>
创建一个SmartPtr<Derived>
,所以就需要对成员 template 产生的函数群进行拣选或筛选:template<typename T> class SmartPtr { public: template<typename U> SmartPtr(const SmartPtr<U>& other) : heldPtr(other.get()) // 以other的heldPtr { ... } // 初始化this的heldPtr T* get() const { return heldPtr; } ... private: T* heldPtr; // 这个SmartPtr持有的内置指针 }; // 使用成员初值列(member initialization list)来初始化 // SmartPtr<T> 之内类型为T* 的成员变量; // 并以类型为U* 指针(由SmartPtr 持有)作为初值。
- 如果声明 member templates 用于泛化 copy 构造或泛化 assignment 操作,还是需要声明正常的 copy 构造函数和 copy assignment 操作符。
条款 46:需要类型转换时请为模版定义非成员函数
- 本条款是在条款 24 的基础上,讲述的有关非成员函数在模板类中(non-member function template)的作用。【条款 24 的 template 版本】
- 条款 24 讲述了我们怎样能实现类的对象在特定条件下的隐式转换问题。【条款 24 是用 non-member】
- template 实参推导过程中从不将隐式类型转换函数考虑在内。
- 在一个 class template 内部,template 名称可被用来作为 template 和其参数的简略表达方式。(比如可以不写
<int>
) - 当我们编写一个 class template,而它所提供之与此 template 相关的函数支持所有参数之隐式类型转换时,请将那些函数定义为 class template 内部的 friend 函数。
条款 47:请使用 traits classes 表现类型信息
- STL 迭代器分类:
- input 迭代器:只能一次一步向前移动,客户只可读取(不能涂写)且只能读取一次它们所指的东西,模仿指向输入文件的阅读指针;例如 istream_iterators。
- output 迭代器:与 input 迭代器类似,但一切只为输出,只能一次一步向前移动,客户只可涂写(不能读取)且只能涂写一次它们所指向的东西,模仿指向输出文件的涂写指针;例如 ostream_iterators。
- forward 迭代器:具有 input 迭代器和 output 迭代器的所有功能,只能一次一步向前移动,可以读或写其所指物一次以上;STL并未提供单向 linked list,但某些程序库有(通常名为 slist),这种容器的迭代器就是 forward 迭代器。
- bidirectional 迭代器:它除了可以向前移动,还可以向后移动,一步只能一次,并可以读或写所指物一次以上;STL 的 list、set、multiset、map 和 multimap 的迭代器就属于这一类。
- random 迭代器:除了 bidirectional 迭代器的所有功能以外,还可以执行迭代器算数,即在常量时间内向前或向后移动任意距离;例如 vector、deque 和 string 的迭代器。
- trait classes 的使用过程如下:
- 建立一组重载函数(身份像劳工)或函数模板,彼此之间的差异仅在于各自的 traits 参数;令每个函数实现码与其接受之 traits 相应和。
- 建立一个控制函数(身份像工头)或函数模板,它调用上述劳工函数并传递 traits classes 所提供的信息。
条款 48:认识 template 元编程
- 模板元编程可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。