7 模版与泛型编程
41 了解隐式接口和编译期多态
面向对象编程主要通过显式接口和运行期多态解决问题。
显式接口:源代码中可见,在头文件中看到的类的接口。
运行期多态:成员函数是virtual,传入类的引用或指针时,在运行时,会自动匹配接口,可能是基类的接口, 也可能是派生类的。
模版和泛型编程主要通过隐式接口和编译期多态解决问题。
隐式接口:typename T,函数中,会有类型T的一组操作,而真正作为实参的类型T,必须支持这一组操作,只有支持这些操作,才能通过编译。
编译期多态:以不同的模版参数具现化函数模版会导致调用不同的函数,这便是所谓的编译期多态。
42 了解typename的双重意义
1 声明template参数的时候,关键字typename和class一样。
2 类中可能会定义类型,如STL容器中定义了iterator,const_iterator等迭代器类型,如果要使用这些类型,是通过类名::类型名来使用的,但是在类中,静态变量也是通过这个方法来使用的,所以当我们明确这是一个类型名(学名为嵌套从属类型名称【P204】)的时候,要使用typename来标志这是一个类型名,例子如下:
template <typename IterT>
void workWithIterator(IterT iter)
{
typedef typename iterator_traits<IterT>::value_type value_type;
value_type tmp(*iter);
...
}
43 学习处理模板化基类的名称
1 模版基类中的普通成员函数不能在派生类中直接被调用,因为可能有的模版基类不包括所有的成员函数,这样编译将会出错,所以编译器不允许模版基类的派生类直接调用模版基类中的成员函数。例子如下:
这是一个模板基类:
template <typename T>
class A{
public:
...
void func1(T &t){ }
void func2(T &t){ }
};
它的特化版本不包括func1()
函数,特化版本的标志是template
的模版参数是空的,这代表这个类既不是模版类也不是标准的C++类,而是特化版本的A
类,在template
参数是specialT
时被使用,这就是所谓的模版全特化。模版全特化的特化是全面性的,也就是说一旦类型参数被定为specialT
,就没有其他template
参数可以变化。
template <>
class A<specialT>{
public:
...
void func1(T &t){ }
void func2(T &t){ }
};
最后是刚才基类的派生类定义:
template <typename T>
class Derived{
...
void funcDerived(T &t){
func1(t); //这里无法通过编译
}
};
无法通过编译的原因是编译器不知道模版基类会不会有一个特化版本(就如全特化版本specialT
)里面没有func1()
这个函数。
2 如果要解决1的问题,要从两点着手:
- 告诉编译器,指定从基类中调用这个成员函数
- 规定特化版本必须支持其泛化版本的所有成员函数
指定从基类中调用成员函数有三个方法:
在基类的函数调用动作之前加上
this->
template <typename T> class Derived{ ... void funcDerived(T &t){ this->func1(t); //使用this-> } };
使用using声明式
template <typename T> class Derived{ using A<T>::func1; //使用using声明 ... void funcDerived(T &t){ func1(t); } };
明确指出被调用的函数位于基类中
这种方法有个缺陷,如果被调用的是虚函数,上述的明确资格修饰会导致虚函数动态绑定失效,因为这里已经指定为基类的函数了。这将导致多态行为的失效。
template <typename T>
class Derived{
...
void funcDerived(T &t){
A<T>::func1(t);
}
};
44 将与参数无关的代码抽离templates
1 编写两个函数如果有相同的部分,可以将相同的部分单独编成函数,并令原先的函数调用这个函数。同样的,如果是两个类有相同得分,可以将这两个相同的部分,单独编成类,然后使用继承或复合。
2 模板类在实例化的时候,如果传入不同参数,在二进制文件中,会产生完全相同的类型码,这么增大可执行文件,所以对模板类来说,需要将参数无关的代码从模板类中拿出来。
对于非类型模板参数造成的代码膨胀,可以消除,做法是以函数参数或类成员变量替换模板参数。
这里的n就是非类型模板参数,实例化模板类的时候,传入不同的n
会生成不同的代码。
template <typename T, int n>
class A{
void func();
};
A<int, 5> a; //产生不同的代码
a.func();
A<int, 5> b;
b.func();
第一种方法是将这个n
单独抽离成函数的参数,再令之前的类继承这个类,通过调用这个类的函数,执行功能,从而完成代码优化。
template <typename T> //抽离出来的基类(去掉非类型模板参数)
class Abase{
void func(int n);
};
template <typename T, int n>
class A : private Abase{
private:
using Abase<T>::func; //防止基类的函数被覆盖
public:
...
void func(){ this->func(n); } //调用基类的函数
};
但是如果要知道执行计算的数据所在的内存的话,基类中不具有这样的功能,所以只能由派生类来完成,但是就要在每一个函数里面都传入一个内存地址,相比与此,我们可以将数据的内存地址放入到类中。这样基类就变成了:
template <typename T> //抽离出来的基类(去掉非类型模板参数)
class Abase{
...
Abase(int, _n, T *pMem)
:n(_n), pData(pMem){ }
void setDataPtr(T *ptr){ pData = ptr }
...
private:
int n;
T *pData;
};
指针可以换做数组存储在对象内部,这不需要动态内存分配,但是会增大对象自身大小。另一种方法就是使用智能指针(unique_ptr
)。
这些代码在传入不同的非类型参数的时候都只有一个func()
函数,这回减少可执行文件的大小,也就降低程序的working set大小,并强化指令高速缓存区内的引用集中化。这会使程序执行更加快速。
working set:对一个在“虚内存环境”下执行的进程而言,其所使用的那一组内存页。
对于类型参数造成的代码膨胀,可以降低其膨胀,做法是让带有完全相同二进制表述的具现类型共享实现码。
例如传入的类型是指针类型的话,如果某些成员函数操作这些强引用类型的指针的话(即T *),可以调用无类型指针函数(即void *),并由这个函数完成实际工作。
45 运用成员函数模板接受所有兼容类型
1 模板类中要使用成员函数模板生成“可接受所有兼容类型”的函数。
例如现在有一个智能指针模板类,智能指针要模拟原始指针的行为(这样智能指针用起来才像一个原始指针),原始指针可以实现基类指针到派生类指针(基类指针指向派生类对象地址)的隐式转换,所以智能指针也要实现类似的功能。
对于智能指针来说,基类和派生类是没有关系的类,所以我们要自行生成拷贝构造函数来实现指针转换。但是基类可能有很多派生类,并且在未来会生成更多的派生类,如果对于每一个派生类都定义对应的拷贝构造函数,那么基类代码将会很多很复杂,所以我们其实是需要为这个智能指针基类写一个构造模板,这个模板就是所谓的成员函数模板。
template <typename T> //模板参数T
class smartPtr{
public:
template <template U> //另一个类型U
smartPtr(const smartPtr<U> &other); //泛化拷贝构造函数
};
这里是根据对象U创建对象T,即对于所有的类型U的智能指针,都可以转换为类型T的智能指针,U和T是同一个模板的不同具现体。这就是泛化拷贝构造函数。
两点注意:
- 不需要声明为显式,即explicit,因为对于原始类指针的上下行转换来说,本身就是隐式的。
- 需要控制函数只能进行类指针的上下行转换,而不能进行任意指针的转换。
对于第2点,可以借助原始指针完成,在智能指针类中声明get()
函数,用于返回原始指针的副本。
template <typename T>
class smartPtr{
public:
template <template U>
smartPtr(const smartPtr<U> &other)
:heldPtr(other.get()){...} //以原始指针副本进行构造,借助原始指针的隐式转换
T *get(){ return heldPtr; } //get函数
...
private:
T *heldPtr; //智能指针内部持有的原始指针
};
通过借助原始指针的隐式转换,我们就获得了智能指针的上下行转换机制。这个行为只有当“仅在某个隐式转换可将一个U*指针转换为一个T*指针”时才能通过编译。
2 成员函数模板不仅仅可以用于构造函数,也可以用于支持赋值操作。
例如shared_ptr类中针对其他智能指针的赋值操作。
template<class Ty>
class shared_ptr {
public:
...
//拷贝构造函数
template<class Other>
explicit shared_ptr(Other * ptr);
template<class Other>
shared_ptr(const shared_ptr<Other>& sp); //泛化拷贝构造函数不需要explicit,因为支持隐式转换
template<class Other>
explicit shared_ptr(const weak_ptr<Other>& wp);
template<class Other>
shared_ptr(auto_ptr<Other>& ap); //不需要const,因为指针值被传递的过程中,原始的auto_ptr被改变了(其实是内部持有的原始指针被删除了)
//赋值符重载
template<class Other>
shared_ptr& operator=(const shared_ptr<Other>& sp);
template<class Other>
shared_ptr& operator=(auto_ptr< Other >&& ap);
...
};
3 声明成员函数模板用于“泛化拷贝构造函数”和“泛化赋值操作”是不会印象正常的拷贝构造函数和赋值操作符的,所以如果有特殊需求,仍然需要自行定义正常的拷贝构造函数和赋值操作符。
46 需要类型转换时轻微模板定义非成员函数
在模版类中如果要进行计算的话,可以定义operator*()
函数,但是即使在类外定义,也是无法实现混合式计算的(即支持交换律)
template <typename T>
class A{
private:
int a;
int b;
public:
A(int _a = 0, int _b = 1) : a(_a), b(_b){ }
int aa(){ return a; }
int bb(){ return b; }
};
template <typename T> //类外定义的模板函数
const A<T> operator(const A<T> &lhs, const A<T> &rhs);
A<int> a(1, 2);
A<int> ret = a * 2; //无法通过编译
这是因为模板类在实参推导的过程中是不考虑隐式转换类型的,也就是说,这里的2是无法通过构造函数被隐式转换为A<int>
类型。
解决方法是使用友元函数,模板类中的友元声明式在类具现化的时候就知道T是什么类型了,但是如果在模板类中声明友元函数,必须直接在类内定义,而不能在类外定义(会导致程序连接出错)。
在类模板中可以出现三种友元声明:
普通非模板类或函数的友元声明,将友元关系授予明确指定的类或函数。
template<class T> class A{ //fun可访问A任意类实例中的私有和保护成员 friend void fun(); ... };
类模板或函数模板的友元声明,授予对友元所有实例的访问权。
template<class T> class A{ //友元使用与类不同的模板形参,U可以是任意合法标志符,友元函数可以访问A类的任何类实例的数据 template<class U> friend void fun(U u); ... };
只授予对类模板或函数模板的特定实例的访问权的友元声明。
template<class T> class A{ //fun只有访问类中特定实例的数据。换句话说,此时具有相同模板实参的fun函数与A类才是友元关系。 friend void fun<T>(T u); ... };
类内部友元函数的模板参数可写可不写,都可以。
template<class T>
class A{
friend A<T> fun(const A<T> &lhs, const A<T> &rhs);
...
};
template<class T>
class A{
friend A fun(const A &lhs, const A &rhs);
...
};
但是类内部定义函数将会被隐式声明为内联函数,所以我们要可以在类外定义函数详细执行流程的函数,然后用类内部的友元函数去调用,这样既不会导致代码的膨胀,也可以解决类外定义无法进行隐式类型推导的问题。
47 请使用traits class表现类型信息
STL的advance()
函数是对迭代器进行移动的函数,因为随机迭代器支持+=/-=
,双向迭代器只支持++/--
,故此函数底层针对随机迭代器和双向、单向迭代器有不同的实现形式:
template<class InputIt, class Distance>
void advance(InputIt& it, Distance n);
但是advance()
对不同迭代器的判断不是发生在运行期(运行期的判断是通过typeid+if…else完成的),而是发生在编译期(这样可以加快程序运行速度,减少程序体积,并且可以将一些原本在运行期发现的错误提前到编译期发现)。
上层提供的接口如下(采用双层结构):
template<typename _InputIterator, typename _Distance>
inline void advance(_InputIterator& i, _Distance n)
{
typename iterator_traits<_InputIterator>::difference_type d = n;
std::__advance(i, d, std::__iterator_category(i));
}
不同的底层实现如下:
1 单向迭代器
template<typename _InputIterator, typename _Distance>
inline void __advance(_InputIterator& i, _Distance n, input_iterator_tag)
{
if(d < 0)
throw exception;
while (n--)
++i;
}
2 双向迭代器
template<typename _BidirectionalIterator, typename _Distance>
inline void __advance(_BidirectionalIterator& i, _Distance n,
bidirectional_iterator_tag)
{
if (n > 0)
while (n--)
++i;
else
while (n++)
--i;
}
3 随机迭代器
template<typename _RandomAccessIterator, typename _Distance>
inline void __advance(_RandomAccessIterator& i, _Distance n,
random_access_iterator_tag)
{
i += n;
}
可以在编译期做到迭代器类型的识别依靠了traits class,是一种迭代器的类型萃取器,这个类型萃取器是通过位于类之外的模板类和特化版本实现的。
//deque是随机迭代器
template <...>
class deque{
public:
class iterator{
public:
typedef random_access_iterator_tag iterator_category;
...
};
...
};
//list是双向迭代器
template <...>
class list{
public:
class iterator{
public:
typedef bidirectional_iterator_tag iterator_category;
...
};
...
};
萃取器萃取其特性,若此时传入list<T>::iterator
类型的迭代器,则内部typedef
的类型为list<T>::iterator::iterator_category
(是bidirectional_iterator_tag
类型),若在上述的advance()
函数中就会匹配相应的类型。
template <typename IterT>
struct iterator_traits{
typedef typename IterT::iterator_category iterator_category;
...
};
指针没有内部typedef,这里我们可以使用模板偏特化给指针添加traits class:
template <typename IterT>
struct iterator_traits<IterT *>{
typedef typename random_access_iterator_tag iterator_category;
...
};
这就是在编译期完成类型的匹配的全过程。
48 认识template元编程
利用模板特化机制实现编译期条件选择结构,利用递归模板实现编译期循环结构,模板元程序则由编译器在编译期解释执行。
模板的声明或定义只能在全局,命名空间或类范围内进行。即不能在局部范围,函数内进行,比如不能在main函数中声明或定义一个模板。
一个模板元编程的样例程序(执行阶乘):
#include <iostream>
using namespace std;
template <unsigned n>
struct factorial{
enum{value = n * factorial<n - 1>::value};
};
template <>
struct factorial<0>{
enum{value = 1};
};
int main()
{
cout << factorial<10>::value << endl;
return 0;
}