模板与泛型编程
本文尝试着介绍对泛型编程的理解,从而扩展我们的template编程。泛型编程是C++中非常重要的一部分,它使得我们节省了很多编写不同代码的体力。
1. 了解隐式接口和编译器多态
与OOP的不同之处
面向对象编程世界总是以显式接口和运行期多态解决问题。
例如:
void doProcessing( Widget &w) {
if (w.size() > 10 && w != someWidget) {
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
- 就像以上的函数,我们总是可以通过查找Widget,发现Widget里面的显式接口,而这些在源码中明确可见(这就是显式接口)。
- 由于Widget的某些成员函数是virtual,w对这些函数的调用将表现出运行期多态,也就是在运行期根据具体的w动态类型决定调用哪个函数。
但在泛型编程的世界里就截然不同了。在此世界里,显式接口和运行时多态仍然存在,只不过不再那么重要了。反而是隐式接口和编译器多态就变得更重要了。我们把上面的函数改成template。
void doProcessing(T &w) {
if (w.size() > 10 && w != someWidget) {
T temp(w);
temp.normalize();
temp.swap(w);
}
}
现在,
- w必须支持哪一种种接口由template中执行在w身上的凑战偶来解决。而这些操作就是T必须支持的一组隐式接口。
- 不同的template参数会在编译器具现化(instantiated)不同的函数从而使得函数可以被调用,这就是所谓的编译器多态。
显式接口和隐式接口
我们进一步的区别这两种接口。
通常,显式接口是由函数的签名式(也就是函数名称,参数类型,返回类型)构成。就好像在一个class的public接口中会有构造函数、析构函数、各类成员函数、还有typedef等等。
隐式接口就不同了。它并不基于函数签名式,而是由有效表达式组成。
void doProcessing(T &w) {
if (w.size() > 10 && w != someWidget) {
T temp(w);
temp.normalize();
temp.swap(w);
}
}
就像前面这个例子,T的隐式接口总有某些约束。
- 它必须提供一个size类型,换回整型。
- 总是swap操作,!=操作符,temp.normalize函数等等。
加诸与template参数上的隐式接口,就像class上的显示接口一样都会在编译器进过检查,从而确保程序的正常运行。
2. 了解typename的双重含义
一般都认为typename和class的意义是相同的,但只在确定template类型时是如此。在以下接种情况下,typename有着特别的意义。
在template指涉(refer to)两种名称
template <typename T>
void print(T& container) {
T::types temp = 10;
cout << temp.get() << endl;
}
以上template实际上是无法通过编译的。我们本来希望temp的类型是T中typedef的类型types,但实际上编译器不这么认为。
在template内出现的名称如果相依于某个template参数,称之为从属名称,如果从属名称在class内呈嵌套状,就叫做嵌套从属名称。T::types实际上就是个嵌套从属名称,并且指涉某个类型。
嵌套从属名称可能会导致解析困难,就比如说
T::types* temp = 10;
编译器会认为types是一个变量名,所以就是types 乘上 temp!很奇怪吧!C++有个规则可以解析此歧义状态:如果解析器在template中遭遇一个嵌套从属名称,它便假设这个名称不是个类型。而准确的做法是用
typename <#qualifier#>::<#name#>
于是函数就应该是:
template <typename T>
void print(T& container) {
typename T::types temp = 10;
cout << temp.get() << endl;
}
并且根据template的隐式接口设计相应的类:
template <typename T>
class Type {
public:
Type(int y) : x(y) {
}
Type& operator =(int &y) {
x = y;
return *this;
}
int get() {
return x;
}
private:
int x;
};
template <typename T>
class Derived {
public:
Derived(int y) : x(y) {
}
void print() {
cout << Base<T>::get() << endl;
}
typedef Type<T> types;
private:
int x;
};
template <typename T>
void print(T& container) {
typename T::types temp = 10;
cout << temp.get() << endl;
}
int main () {
Derived<int> temp(10);
print(temp);
return 0;
}
/*
output:
10
*/
所以一般地规则就是:任何时候当你想要在template中指涉一个嵌套从属类型名称,就应该在其前面加上关键字typename。
不可以使用typename的情况
还有一个意外情况就是:typename不可以出现在base class list内的嵌套从属类型名称之前,也不在member initialization list中作为base class修饰符。
所以使用继承关系的template就应该是如下的语法:
template <typename T>
class Base {
public:
Base(int y) : x(y) {
}
int get() {
return x;
}
private:
int x;
};
template <typename T>
class Type {
public:
Type(int y) : x(y) {
}
Type& operator =(int &y) {
x = y;
return *this;
}
int get() {
return x;
}
private:
int x;
};
template <typename T>
class Derived : public Base<T> {
public:
Derived(int x) : Base<T>::Base(x) {
}
void print() {
cout << Base<T>::get() << endl;
}
typedef Type<T> types;
};
另外,因为每一次使用嵌套从属类型名称都是打一大串的代码,所以为了防止出错,最好使用typedef把相应的代码简化。
3. 学习处理模板化基类内的名称
假设我们需要对不同company设计不同类来保存各自的信息,并且用一个类来发送信息。
class companyA {
public:
companyA(string msgs) : msg(msgs) {
}
void print_message() {
cout << msg << endl;
}
private:
string msg;
};
class companyB {
public:
companyB(string msgs) : msg(msgs) {
}
void print_message() {
cout << msg << endl;
}
private:
string msg;
};
template <typename Company>
class MsgSender {
public:
void Send(Company& orig) {
orig.print_message();
}
};
template <typename Company>
class Loger : public MsgSender<Company> {
public:
void Send_msg(Company& orig) {
Send(orig); // error!
}
};
int main () {
companyA temp("yan");
companyB tempB("ze");
Loger<companyA> tempA;
tempA.Send_msg(temp);
return 0;
}
这个类是有问题的。原因就在于编译器找不到关于Send函数的定义。就算很明显的在基类中有Send函数。原因就在于当编译器遇到Loger的定义式时,并不知道将继承什么样的class,就算知道是继承MsgSender,它也不知道这个类中是否有Send这个函数。
具体的说,就是有个特别的类,如:
class companyC {
public:
companyC(string msgs) : msg(msgs) {
}
void print() {
cout << msg << endl;
}
private:
string msg;
};
使得MsgSender没有办法正常地调用print_message()函数,于是出错。解决方法就是特化模板:
template <>
class MsgSender<companyC> {
public:
void Send(companyC& orig) {
orig.print();
}
};
问题还没有解决完。编译器仍然找不到关于Send的定义。因为编译器往往拒绝在template base class内寻找继承而来的名称,所以解决方法就是强制他调用。
方法一:加上this->
template <typename Company>
class Loger : public MsgSender<Company> {
public:
void Send_msg(Company& orig) {
this->Send(orig);
}
};
方法二:使用using声明式
template <typename Company>
class Loger : public MsgSender<Company> {
public:
using MsgSender<Company>::Send;
void Send_msg(Company& orig) {
Send(orig);
}
};
方法三:明确指出调用函数的作用域
template <typename Company>
class Loger : public MsgSender<Company> {
public:
void Send_msg(Company& orig) {
MsgSender<Company>::Send(orig);
}
};
这个方法往往最不好,因为他限制了virtual绑定行为。
完整的实例程序:
#include <iostream> // std::cout
using namespace std;
class companyA {
public:
companyA(string msgs) : msg(msgs) {
}
void print_message() {
cout << msg << endl;
}
private:
string msg;
};
class companyB {
public:
companyB(string msgs) : msg(msgs) {
}
void print_message() {
cout << msg << endl;
}
private:
string msg;
};
class companyC {
public:
companyC(string msgs) : msg(msgs) {
}
void print() {
cout << msg << endl;
}
private:
string msg;
};
template <typename Company>
class MsgSender {
public:
void Send(Company& orig) {
orig.print_message();
}
};
template <typename Company>
class Loger : public MsgSender<Company> {
public:
void Send_msg(Company& orig) {
MsgSender<Company>::Send(orig);
}
};
template <>
class MsgSender<companyC> {
public:
void Send(companyC& orig) {
orig.print();
}
};
int main () {
companyA tempA("yan");
companyB tempB("ze");
companyC tempC("xin");
Loger<companyA> temp;
temp.Send_msg(tempA);
return 0;
}
/*
output:
yan
*/
4. 运用成员函数模板接受所有兼容类型
真实指针做得很好的意见事情是,支持隐式类型转换,从而实现了动态绑定。但是在template中却并不会自动具有这个功能,需要我们自己写出成员函数模板。
问题产生
本节主要探讨智能指针初始化的问题。
假设我们需要做一个继承体系:
class Top { ... };
class Middle : public Top { ... };
class Bottom : public Middle { ... };
Top* pt1 = new Middle;
Top* pt2 = new Bottom;
const Top* pct2 = pt1;
如果我们想要写出一个SmartPtr的类来完成对智能指针的封装,我们需要注意一些问题。在同一个template的不同具现化之间并不存在什么与生俱来的固有关系(就比如说,带有base-dereived关系的b、d两类型分别具现化某个template,产生出来的是两个不带有base-dereived关系的具现化类),所以编译器并不认为SmartPtr和SmartPtr之间有什么关系。所以为了获得转换关系,我们需要明确地写出来。
Template和泛型编程
简单地说,根据泛型编程的思维,我们总是希望能够写出一个泛型的构造函数并附加一些判断(其是否具有base-dereived关系)来满足各式各样的情况。方法就是使用成员函数模板!
template SmartPtr {
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other) :
heldPtr(other.get()) { ... }
T* get() const { return heldPtr; }
...
private:
T* heldPtr;
}
这个代码中必须注意,不能写成explicit的,因为我们总是希望能够进行隐式转换。其次我们用other的成员get返回一个该类型的指针,在此初始化中,编译器会判断这两个指针之间是否可以隐式转换,从而确认这两者之间是否真的有相应关系。
shared_ptr摘录
泛化copy构造函数具现化原理
member function template是个奇妙东西,它并不改变语言基本规则,而这意味着,你声明了一个泛化copy构造函数,并不阻止编译器生成他们自己的copy构造函数。也就是说,如果你写了一个泛化的copy构造函数,而此时恰好U和Y的类型恰好相同,编译器会调用自己生成的copy构造函数。所以为了能够准确的控制类。最好还是两种copy 构造函数都写上吧。
5. 需要类型转换时请为模板定义非成员函数
本节我们讨论如何把下面这个函数转换为template。
class Divd {
public:
Divd(int n1s = 0, int n2s = 1) : n1(n1s), n2(n2s) {
}
int n1;
int n2;
};
Divd operator*(const Divd& orig1, const Divd& orig2) {
Divd temp(orig1.n1 * orig2.n1, orig1.n2 * orig2.n2);
return temp;
}
int main () {
Divd temp = Divd(4, 2) * 2;
cout << temp.n1 << "," << temp.n2 << endl;
return 0;
}
/*output:
8, 2
*/
问题产生
一开始我们认为这个问题非常简单:
template <typename T>
class Divd {
public:
Divd(int n1s = 0, int n2s = 1) : n1(n1s), n2(n2s) {
}
int n1;
int n2;
};
template <typename T>
Divd<T> operator*(const Divd<T>& orig1, const Divd<T>& orig2) {
Divd<T> temp(orig1.n1 * orig2.n1, orig1.n2 * orig2.n2);
return temp;
}
// in main:
...
Divd<int> temp = Divd<int>(4, 2) * 2;
但随后我们发现这个程序编译无法通过。问题就在于模板化的divd和non-template的divd是不同的。理解这个问题我们需要仔细分析operator*调用中的实参类型推断。
Divd<int>(4, 2)
这一部分的推导并不困难,说明T就是int了。
但是第二个参数开始出现问题了。本来第二个参数应该是const Divd& 的类型,但调用过程却给出了int,编译器该如何推算出T?我们当然希望避免能够隐式转换把int转换为const Divd&,但因为在template实参推导过程中从不将隐式类型转换函数纳入考虑。这样的转换在函数调用过程中的确被使用,但在调用一个函数之前,首先必须知道那个函数存在,而为了知道他,必须先为相关function template推导出相关的参数类型,然而在template实参推导过程中并不考虑采纳“通过构造函数而发生的”隐式类型转换,于是以失败告终。
解决方法:使用friend
在template class内的friend声明式可以指涉某个特定函数。class template并不依赖template实参推导(后者指用子啊function template),所以编译器总是能够在class divd具现化是得知T。还有一个问题就是,如果我们声明了一个函数,就应该给出相应的实现,所以operator*的实现代码必须写进class中,否则会出现连接错误。
正确代码:
template <typename T>
class Divd {
public:
Divd(int n1s = 0, int n2s = 1) : n1(n1s), n2(n2s) {
}
int n1;
int n2;
friend Divd operator*(const Divd& orig1, const Divd& orig2) {
Divd temp(orig1.n1 * orig2.n1, orig1.n2 * orig2.n2);
return temp;
}
};
最后补充一点,把代码写进class声明式中,意味着隐式地把它声明为inline,如果代码量太大时,最后是在class外再写一个具体的实现过程,然后在class内的函数中调用那个函数,从而减少目标码的量。
6. 使用traits class表示类型信息
trait class表示一个类所具有的类型信息,这种class在STL中广泛使用。
问题产出
STL主要由“泳衣表现容器、迭代器和算法”的template构成,但也覆盖若干工具性的template,其中一个为advance,用来将某个迭代器移动某个距离。
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d);
我们希望这个advance能够一步到位,达到我们希望到达的iter。但事实上,并不是每一种迭代器都能使用+=操作。首先我们先来介绍了一下迭代器的分类。
迭代器分类
STL公有5中迭代器分类,对应他们支持的操作。
- input迭代器:只能向前移动,一次一步,客户只能读取(不可写)他们所指的东西,而且只能读一次。(istream_iterator)
- output迭代器:一切只为输出,只能向前移动,一次一步,客户只可涂写所指的东西,而且只能涂写一次。(ostream_iterator)
(以上两种迭代器只适合“一次性操作算法)
- forward迭代器:这种迭代器可以做上述两种迭代器的事情,而且可以读或写其所指物一次以上,这可施行于多次性操作算法。
- bidirectional迭代器:这种迭代器可以双向移动(例如list、set、map等的迭代器)
- random access迭代器,可以执行迭代器算法。(可以在常量时间向前或向后移动任意距离)
对于以上5中迭代器,C++标准程序库分别提供专属的卷标结构(tag struct)
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};
问题解决
我们回到原来的advance里。
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
if ( /* iter is a random access iterator) {
iter += d;
} else {
if (d >= 0) {
while (d--) {
iter++;
}
} else {
while (d++) {
iter--;
}
}
}
}
这我们希望实现的样子。所以问题的关键在于我们要知道IterT是否为random access迭代器分类,也就是知道这个类型的某些信息。那就trait做的事:他们允许我们在编译器间区别某些类型信息。
Trait并不是c++关键字或一个预先定义好的构件:他们是一种计数,也是一个c++程序员共同遵守的协议。它的技术要求之一是,他对内置类型和用户自定义类型的表现必须一样好。(也就是说,就算传进去的是const char* 和 int,也一样能够正常的运行)
因为”trait必须能够施行于内置类型“意味着”类型内的嵌套信息“这种东西是无法实现的,因为我们无法将信息嵌套于原始指针内,因此类型的trait信息必须位于类型之外。标准技术是把它放进一个template及其一个或多个特化版本。
在deque里面嵌套一个iterator类,并且在这个iterator里面声明一个iterator_category用来确认IterT的迭代器分类。
template<...>
class deque {
public:
class iterator {
public:
typedef random_access_iterator_tag iterator_category;
...
};
...
};
至于iterator_traits。
template<typename IterT>
struct iterator_traits {
typedef typename IterT::iterator_category iterator_category;
...
};
这对用户自定义类型是行得通的,但对指针是行不通的,因为指针不可能嵌套typedef。所以我们需要针对指针提供偏特化版本。
template<typename IterT>
struct iterator_traits<IterT*> {
typedef random_access_iterator_tag iterator_category;
...
};
现在我们知道应该如何设计并实现一个trait class了:
- 确认若干你希望将来可取得的类型相关信息。例如迭代器而言,我们希望将来可取的其分类。
- 为该信息选择一个名称。
- 提供一个template和一组特化版本,内含你希望支持的类型相关信息。
所以之前的advance可以这么实现:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d) {
if (typeid(typename std::iterator_traits<IterT>::iterator_category) == typeid(std::random_access_iterator_tag)) {
...
}
...
}
这虽然看起来可以完成任务,但会导致编译问题。因为在编译器中IterT类型可知,std::iterator_traits::iterator_category的类型也可知,但if语句却是在运行期才会核定。这不仅浪费时间,也造成可执行文件膨胀。
所以我们需要一个条件式,判断“编译器核定成功”之类型。而重载就可以完成任务。
photo:
有了这些doAdvance重载版本,advance需要做的只是调用它们并额外传递一个对象,后者必须带有适当的迭代器分类。于是编译器运用重载解析机制调用适当的实现代码:
photo:
我们可以总结如何使用traits class了。
- 建立一组重载函数或函数模板,彼此间的差异只在于各自的trait参数。令每个函数实现码与其接受之traits信息相应和。
- 建立一个控制函数或函数模板,它调用上述那些重载函数并传递trait class所提供的信息。
traits广泛用于标准程序库,所以了解这个机制对我们理解库有重要的意义。