41: 了解隐式接口与编译期多态
面向对象中的类设计时需要考虑显式接口和运行时多态,而模板编程中需要考虑隐式接口和编译器多态
- 如果函数的形参是普通类:
- 普通类的显式接口由函数签名(函数名、形参类型、返回值类型)表征,运行时多态由虚函数实现
- 在函数进行编译时,就可以知道该普通类有哪些接口
- 如果函数的形参是模板类型:
- 模板类型的隐式接口由表达式的合法性表征(即该模板类型应该支持函数中形参调用的方法),编译器多态由模板初始化和重载函数的解析实现
- 在函数进行编译时,无法知道模板类型有哪些接口,因此视为鸭子类型(即传入对象支持函数中调用的方法即可)
- 在编译函数时当然无法确定模板类型,但是当传入实参后,内部如果调用了实参未定义的函数,同样会在编译期报错而非运行期
- 参考:
42: 了解typename
的双重意义
- 在模板声明中,使用
class
与typename
完全相同 - 在模板内部,
typename
还可以用来显式指明【嵌套从属类型名称】- 背景:比如编译器无法在模板内部判断
T::mem
是一个static成员(默认),还是一个类型 - 嵌套从属类型名称:
T::mem
是一个依赖于模板参数T
的类型 - 例子:模板内部
typename T::age myAge = 25;
typename
还可以用来显式指明【嵌套从属类型名称】,可以出现在模板内部、函数形参列表,但是不可以出现在【类派生列表】和【构造函数中成员初始化列表】中- 当类型名称过于复杂时,可以使用类型别名
- 背景:比如编译器无法在模板内部判断
- 参考
43: 使用模板化基类中的成员函数
- 背景:如果基类是一个模板类,派生类进行继承
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
class Buff {}; class RedBuff: public Buff {}; class BlueBuff: public Buff {}; template <typename T> // 基类 class Container { /* 假设有成员函数func() */}; //template <> // 全特化基类 //class Container<Buff*> { /* 假设没有成员函数func() */ }; // template <typename T=Buff*> // 使用默认模板实参,同全特化基类 // class Container<T> { /* 假设没有成员函数func() */ } template <typename T> class PlayerContainer: public Container<T> { // 派生类继承模板化基类 public: void test() { func(); } // 这里编译报错 };
- 对于模板化基类,可能有特化版本,且其中可能有不同的接口
- 对于派生类而言,也无法确定类型T,因此C++规定派生类不在模板化基类中查找继承而来的接口
- 解决方法:向编译器承诺所有的特化版本都遵循模板化基类的接口(或者说使用非特化版本的模板化基类中的接口)
- 使用
this
显式指出访问基类的成员函数1 2 3 4 5
template <typename T> class PlayerContainer: public Container<T> { // 派生类继承模板化基类 public: void test() { this->func(); } // this指针可以访问所有成员函数 };
- 使用
using
声明1 2 3 4 5 6
template <typename T> class PlayerContainer: public Container<T> { // 派生类继承模板化基类 public: using Container<T>::func; // 告诉编译器,func在模板化基类中 void test() { func(); } };
- 使用作用域运算符
::
明确指出,不推荐使用,因为如果func是虚函数,使用这种方法不会产生多态1 2 3 4 5
template <typename T> class PlayerContainer: public Container<T> { // 派生类继承模板化基类 public: void test() { Container<T>::func(); } // 明确指出 };
- 使用
44: 将与参数无关的代码抽离模板
- 背景:代码膨胀
- 模板提供的是编译期多态,不同的类型参数会生成不同的模板
- 比如一个模板类接受一个类型参数T与一个非类型参数N,大部分成员都使用类型参数T,只有极少部分成员使用非类型参数N
- 如果使用相同的类型type、但是不同的非类型参数n进行实例化,生成的代码中大部分都相同,只有极少部分不同
- 抽取公共代码:
- 模板中生成的冗余代码是隐式的,因为模板只有一份,生成不同实例后才可能产生冗余
- 比如可以将与参数无关的代码(成员函数,数据成员)放入基类中,然后private继承
- 参考
45: 运用成员函数模板接受所有兼容类型
- 背景:假如类型参数T存在继承关系,但是模板实例化后是完全不同的两个类
- 比如有一个继承体系,基类Base,派生类Derived
- 指向派生类的指针可以转换为指向基类的指针:
Base* p = new Derived();
- 但是指向派生类的智能指针无法转换为指向基类的智能指针:
shared_ptr<Base*> sp = make_shared<Derived*>(new Derived());
- 重载构造函数
- 接受同一模板的其他实例的构造函数称为通用构造函数
- 兼容类型检查:将
MySmartPtr<U>
转换为MySmartPtr<T>
,前提是类型U可以转换为类型T - 如果没有声明拷贝构造函数,编译器会自己生成一个,而非使用通用构造函数去进行成员模板实例化
1 2 3 4 5 6 7 8 9 10 11 12 13
template <typename T> class MySmartPtr{ public: MyShartPtr(T* p): ptr(p) {} template <typename U> MySmartPtr(const MySmartPtr<U>& other): ptr(other.get()) {}; // 带类型兼容检查的通用构造函数,可以实现隐式类型转换(因为不带explicit) T* get() const {return ptr;} private: T *ptr; }; MySmartPtr<Derived*> dp(new Derived()); // 隐式类型转换 MySmartPtr<Base*> bp = MySmartPtr<Derived*>(new Derived()); // T=Base*, U=Derived*
- 参考
46:需要类型转换时请将模板定义为非成员函数
- 背景:
1 2 3 4 5 6 7 8
template <typename T> class Rational {}; template <typename T> const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) {} Rational<int> oneHalf(1,2); Rational<int> result = oneHalf * 2; // Error
- 模板函数的调用过程:
- 首先推导出类型T,将函数进行实例化:此时无法从2推导得出类型T
- 在调用时,有的参数可能需要隐式类型转换
- 解决方法:将模板函数定义为类的友元,因此类模板实例化后类型T已知
- 如果仅仅是声明,编译器不会对友元函数进行实例化,因此需要进行定义
- 定义在类内部的函数是inline的,可以在类外部定义一个辅助函数(也是模板函数,但是不需要隐式类型转换)
1 2 3 4 5 6 7 8 9 10
template <typename T> class Rational; template <typename T> const Rational<T> func(const Rational<T>& lhs, const Rational<T>& rhs) {} template <typename T> class Rational{ public: friend Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs){ func(lhs, rhs); // 可以推导出类型T,而且不需要进行隐式类型转换 } }
47: 请使用traits classes表现类型信息
- 使用Traits的特点:
- 可以同时支持自定义类型和基础类型
- 在编译期就获取信息
C++中的Traits类可以在编译期提供类型信息,是通过Traits模板及其特化来实现的
C++标准库中提供了不同的Traits:iterator_traits
,char_traits
,numeric_limits
等(以iterator_traits
为例)
- 背景:容器与算法通过迭代器联系在一起,算法中可能需要知道迭代器的类型、迭代器中元素的类型,由此有不同的处理方法
- 比如算法
advance
可以让一个迭代器移动n步(负数则反向移动)- 迭代器有五种:其中随机访问迭代器可以直接使用
+=
操作- C++提供了五个类标识迭代器类型:
input_iterator_tag
,output_iterator_tag
,forward_iterator_tag
,bidirectional_iterator_tag
,random_access_iterator_tag
- C++提供了五个类标识迭代器类型:
- 传入的参数也可能是基本类型的指针
1 2 3 4 5 6 7 8
template <typename IterT, typename DistT> void advance(IterT& iter, DistT d){ // 判断迭代器类型 if(iter is random access iterator) iter += d; else ... // 判断迭代器中元素类型 if(iter.value_type is MyVector) cout<<"MyVector"<<endl; }
- 迭代器有五种:其中随机访问迭代器可以直接使用
- 分析:
- 如果
IterT
是类类型,因此可以在类中携带数据成员,表示迭代器类型和元素类型 - 但是
IterT
也可能是基本类型的指针类型,无法在其中携带信息
- 如果
- Traits技法:使用Traits可以通过一个模板类间接获取
IterT
的相关信息1 2
template <typename IterT> struct my_iterator_traits;
- 比如算法
- Traits是C++中一种编程惯例,允许在编译期得到类型的信息
- traits是一个用来携带信息的很小的类,需要实现两个部分:
- traits中的类型可能是用户自定义的类型,
- 自定义类型中需要实现相应的迭代器,对具体的类型信息起一个通用的别名
- traits中包装相应的信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
template <typename T> class MyVector{ // 自定义类 public: class iterator{ // 自定义类中的迭代器 public: using value_type = T; using iterator_category = my_random_access_iterator_tag; }; }; // iterator_traits可以获取迭代器(或指针类型)的元素类型和迭代器类型(指针类型视为随机访问迭代器) template <typename IterT> struct iterator_traits{ // IterT是类类型 using iterator_category = typename IterT::iterator_category; using value_type = typename IterT::value_type; };
- traits中的类型可能是基本数据类型,遵循相同的名称,包装一下相应的信息
1 2 3 4 5
template <typename IterT> struct iterator_traits<IterT*>{ // 特化版本:IterT是基本类型,IterT是基本类型的指针 using iterator_category = my_random_access_iterator_tag; // 指针可以使用+=操作,因此视为随机访问迭代器 using value_type = IterT; };
- traits中的类型可能是用户自定义的类型,
- traits是一个用来携带信息的很小的类,需要实现两个部分:
- 使用
- 不好的写法:使用
typeid
在运行时判断类型- 但是IterT类型在编译期就可以确定,对象iter的类型需要在运行时确定
- 更严重的问题:静态类型检查(编译期必须确保所有源码都有效,即使是不会执行的源码) ^826df6
- 比如即使迭代器不是
my_random_access_iterator_tag
类型,编译期也会进入if语句测试该迭代器是否支持+=运算,不支持的话编译报错
- 比如即使迭代器不是
1 2 3 4 5
template <typename IterT, typename DistT> void advance(IterT& iter, DistT d){ if( typeid(typename std::iterator_traits<IterT>::iterator_category) == typeid(my_random_access_iterator_tag) ) iter += d; }
- 推荐实现方法:根据不同的类型创建不同的重载方法(worker),然后在一个master函数中调用,依据traits类型进行重载调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
template <typename IterT, typename DistT> void advance(IterT& iter, DistT d){ // 将IterT中迭代器类型和元素类型萃取出来 std::cout<<typeid(typename my_iterator_traits<IterT>::iterator_category).name()<<std::endl; std::cout<<typeid(typename my_iterator_traits<IterT>::value_type).name()<<std::endl; // 错误使用:如果iter是指针类型,则IterT为基本类型,无iterator_category属性 // std::cout<<typeid(IterT::iterator_category).name()<<std::endl; doAdvance(iter, d, typename my_iterator_traits<IterT>::iterator_category()); // 最后默认初始化一个iterator_category的对象,进行重载匹配,调用对应的函数 } // 随机访问迭代器版本 template <typename IterT, typename DistT> void doAdvance(IterT& iter, DistT d, my_random_access_iterator_tag){ iter += d; } // 前向迭代器版本 template <typename IterT, typename DistT> void doAdvance(IterT& iter, DistT d, my_forward_iterator_tag){ assert(d >= 0 && "d must be not less then 0"); while(d--) ++iter; }
- 不好的写法:使用
- 测试代码
|
|
48: 认识模板元编程
- 模板元编程(template metaprogramming,TMP):编写模板,执行于编译期,生成具象化的代码
- 优点:可以将很多工作从运行期转移到编译期
- 一些错误可以提前发现
- 运行时更高效:可执行文件体积小,运行期短,内存需求少
- 避免了[[ch07-模板与泛型编程#^826df6|静态类型检查]]的问题
- 缺点:编译时间变长
- 优点:可以将很多工作从运行期转移到编译期
- 模板元编程
- 图灵完备
- 循环由递归实现