七、模板和泛型编程
(41)、了解隐式接口和编译器多态
面向对象编程总是以显式接口(explicit interfaces)和运行期多态(runtime polymorphism)解决问题。
Templates及泛型编程的世界,隐式接口(implicit interfaces)和编译器多态(compile-time polymorphism)移到前头了。
template<typename T>
void doProcessing(T& w)
{
if(w.size() > 10 && w != someNastWidget)
{
T temp(w);
temp.normalize();
temp.swap(w);
}
}
w必须支持哪一种接口,系由template中执行于w身上的操作来决定。本来来看w的类型T好像必须支持size,normalize和swap成员函数、copy构造函数(用以建立temp)、不等比较(inequality comparism,用来比较someNasty-Widget)。这一组表达式(对此template而言必须有效编译)便是T必须支持的一组隐式接口(implicit interface)。
凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化(instantiated),使这些调用得以成功。这样的具现行为发生在编译器,“以不同的template参数具现化function templates”会调用不同的函数,这便是所谓的编译器多态(compile-time polymorphism)。
通常显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。例如Widget class:
class Widget {
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
}
隐式接口就完全不同了。它并不基于函数签名式,而是由有效表达式(valid expression)组成。再次看看doProcessing template一开始的条件:
template<typename T>
void doProcessing(T& w)
{
if(w.size() > 10 && w != someNastWidget)
{
...
T(w的类型)的隐式接口看来好像有这些约束:
它必须提供一个名为size的成员函数,该函数返回一个整数值。
它必须支持一个operator!=函数,用来比较两个T对象。这里我们假设someNastyWidget的类型为T。
由于操作符重载带来的可能性,这两个约束都不需要满足。是的,T必须支持size成员函数,然而这个函数也可能从base calss继承而得。这个成员函数不需返回一个整数值,甚至不需返还一个数值类型。就此而言,它甚至不需要返回一个定义有operator>的类型!它唯一需要做的是返回一个类型为X的对象,而X对象加上一个int(10的类型)必须能够调用一个operator>。这个operator>不需要非得取得一个类型为X的参数不可,因为它也可以取得类型Y的参数,只要存在一个隐式转换能够将类型X的对象转换为类型Y的对象!operater!=也是同样的道理。
加诸于template参数身上的隐式接口,就像加诸于class对象身上的显示接口一样真实,而且两者都在编译期完成检查。
请记住:
classes和templates都支持接口(interface)和多态(polymorphism)。
对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。
对template参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。
(42)、了解typename的双重意义
以下template声明中,class和typename有什么不同?
template<class T> class Widget;
template<typename T> class Widget;
答案:没有不同。当我们声明template类型参数,class和typename的意义完全相同。
template<typename C>
void print2nd(const C& container)
{
if(container.size() >= 2)
{
C::const_iterator iter(containter.begin());
++iter;
int value = *iter;
std::cout << value;
}
}
iter的类型是C::const_iterator,实际是什么必须取决于template参数C。template内出现的名称如果相依于template参数,称之为从属参数(dependent names)。如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称。C::const_iterator就是这样一个名称。
print2nd内的另一个local变量value,其类型是int。int是一个并不依赖任何template参数的名称。这样的名称是谓非从属名称(non-dependent names)。
在我们知道C是什么之前,没有任何办法可以知道C::const_iterator是否为一个类型。而当编译器开始接受template print2nd时,尚未确知C是什么东西。C++一个规则可以解析(resolve)此一歧义状态:如果解析器在template中遭遇一个嵌套从属名称,它便假设这个名称不是个类型,除非你告诉它是。当然此规则有个例外。
因此下面的代码才是合法的(C::const_iterator如果是类型):
template<typename C>
void print2nd(const C& container)
{
if(container.size() >= 2)
{
typename C::const_iterator iter(containter.begin());
...
}
}
一般性规则很简单:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字typename。
“typename必须作为嵌套从属类型名称的前缀词”这样规则的例外是,typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class修饰符。例如:
template<typename T>
class Derived : public Base<T>::Nested { //base class list中不允许“typename”
public:
expilcit Derived(int x)
: Base<T>::Nested(x) //mem. ini. list中不允许“typename”
{
typename Base<T>::Nested temp; //嵌套从属类型名称
... //既不在base class list中也不在mem. init. list中,作为一个base class修饰符需加 //上typename。
}
...
};
下面是一个真实程序中代表性例子:
template<typename IterT>
void workWithIterator(IterT iter)
{
typename std::iterator_traits<IterT>::value_type temp(*iter);
...
}
请记住:
声明template参数时,前缀关键字class和typename可互换。
请使用关键字typename标识嵌套从属类型名称;但不得在base class list(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。
(43)、学习处理模板化基类内的名称
class CompanyA {
public:
...
void sendClearText(const std::string& msg);
void sendEncrypted(const std::string& msg);
...
};
class CompanyB {
public:
...
void sendClearText(const std::string& msg);
void sendEncrypted(const std::string& msg);
...
};
... //针对其他公司设计的classes
class MsgInfo { ... } //这个class用来保存信息,以备将来产生信息
template<typename Company>
class MsgSender {
public:
... //构造函数、析构函数等等
void sendClear(const MsgInfo& info)
{
std::string msg;
在这儿,根据info产生信息;
Company c;
c.sendClearText(msg);
}
void sendSecret(const MsgInfo& info) //类似sendClear,唯一不同是这里调用c.sendEncrypted
{ ... }
}
下面是加上对日志信息的管理:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
... //构造函数、析构函数等等
void sendClearMsg(const MsgInfo& info)
{
将“传送前”的信息写至log;
senderClear(info); //调用base class函数;这段代码无法通过编译
将“传送后”的信息写至log;
}
...
};
这段代码无法通过编译,问题在于,当编译器遭遇class template LoggingMsgSender定义式时,并不知道它继承什么样的class。
为了让问题更具现化,假设我们有个class CompanyZ坚持使用加密通讯:
class CompanyZ { //这个class不提供sendClearText函数
public:
...
void sendEncrypted(const std::string& msg);
...
}
一般性的MsgSender template对CompanyZ并不合适,我们可以针对CompanyZ产生一个MsgSender特化版:
template<>
class MsgSender<CompanyZ> //一个全特化的MsgSender;它和一般template相同,差别只在于它删掉了 //sendClear。
class MsgSender<CompanyZ> {
public:
...
void sendSecret(const MsgInfo& info)
{ ... }
};
现在如果以CompanyZ调用LoggingMsgSender类,由于sendClear函数不存在,将会出错。那就是C++拒绝这个调用的原因:它知道base class template有可能被特化,而那个特化版本可能不提供和一般性template相同的接口。就某种意义而言,当我们从Object Orinted C++跨进Template C++,继承就不像以前那般畅行无阻了。
为了重头来过,我们必须有某种办法令C++“不进入templated base classes观察”的行为失效。有三个办法,第一是在base class函数调用动作之前加上“this->”:
template<typename Company>
public:
... //构造函数、析构函数等等
void sendClearMsg(const MsgInfo& info)
{
将“传送前”的信息写至log;
this->senderClear(info); //成立,假设sendClear将被继承
将“传送后”的信息写至log;
}
...
};
第二是使用using声明式。
template<typename Company>
public:
using MsgSender<Company>::senderClear; //告诉编译器,请它假设senderClear位于base class内
... //构造函数、析构函数等等
void sendClearMsg(const MsgInfo& info)
{
将“传送前”的信息写至log;
senderClear(info); //OK,假设sendClear将被继承下来
将“传送后”的信息写至log;
}
...
};
第三个做法是,明白指出被调用的函数位于base class内:
template<typename Company>
public:
... //构造函数、析构函数等等
void sendClearMsg(const MsgInfo& info)
{
将“传送前”的信息写至log;
MsgSender<Company>::senderClear(info); //OK,假设sendClear将被继承下来
将“传送后”的信息写至log;
}
...
};
但这往往是最不让人满意的一个解法,因为如果被调用的是virtual函数,上述的明确资格修饰(explicit qualification)会关闭“virtual绑定行为”。
从名称可视点(visibility point)的角度出发,上述每一个解法做的事情都相同:对编译器承诺“base class template的任何特化版本都将支持其一般(泛化)版本所提供的接口”。
根本而言,本条款探讨的是,面对“指涉base class member”之无效reference,编译器的诊断时间,可能发生在早期(当解析derived class template的定义式时),也可能发生在晚期(当那些template被特定之tempalte实参具现化时)。C++的政策是宁愿早诊断,这就是为什么“当base class从template中被具现化时”它假设他对那些base classes的内容毫无所悉的缘故。
请记住:
可在dervied class templates内通过“this->”指数base class template内的成员名称,或藉由一个明白写出的“base class资格修饰符”完成。
(44)、将与参数无关的代码抽离templates
使用template可能会导致代码膨胀(code bloat):其二进制带着重复(或几乎重复)的代码、数据,或两者。其结果有可能源码看起来合身而整齐,但目标吗(object code)却不是那么回事。
template<typename T,std::size_t n>
class SquareMatrix {
public:
...
void invert();
...
};
SquareMatrix<double,5> sm1;
sm1.invert();
SquareMatrix<double,10> sm2;
sm2.invert();
...
这两份代码并非完全相同,这就是tempalte代码膨胀的典型例子。
下面是对代码的第一次修改:
template<typename T>
class SquareMatrixBase {
public:
...
void invert(std::size_t maxtrixSize);
...
};
template<typename T,std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
private:
using SquareMatrixBase<T>::invert; //避免遮掩base版的invert
public:
...
void invert() { this->invert(n); } //制造一个inline调用,调用base class版的invert,时候说明为什么这儿出 ... //现this->
};
这样就共享SquareMatrixBase 唯一一个class内的invert。这个函数使用“this->”记号,因为若不这样做,模板化基类内的函数名称会被derived classes掩盖(条款43)。
看着一切都好,但是有个棘手的问题就是SquareMatrixBase::invert如何操作数据,如何与derived class互动。可能需要指针,指向一块用来放置矩阵数据的内存起始点。这样将添加额外的参数,却得一次一次的告诉SquareMatrixBase相同的信息,这样似乎不好。
另外一种办法是令SquareMatrixBase存一个指针,指向矩阵数据所在内存。
template<typename T>
class SquareMatrixBase {
protected:
SquareMatrixBase(std::size_t n,T* pMem) //存储矩阵大小和一个指针,指向矩阵数值。
:size(n),pData(pMem) { }
void setDataPtr(T* ptr) { pData = ptr; } //重新赋值给pData.
void invert(std::size_t maxtrixSize);
...
private:
std::size_t size ; //矩阵大小
T* pData; //指针,指向矩阵内容
};
这允许derived classes决定内存分配方式。某些实现版本也许会决定将矩阵数据存储在SquareMatrix对象内部:
template<typename T,std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
public:
SquareMatrix() //送出矩阵大小和数据指针给base class
: SquareMatrixBase<T>(n,data) { }
...
private:
T data[n*n];
};
这种类型的对象不需要动态分配内存,但对象自身可能非常大。另外一种做法是把每一个矩阵的数据放进heap(也就是通过new来分配内存):
template<typename T,std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
public:
SquareMatrix() //将base class的数据指针设为null,
: SquareMatrixBase<T>(n,0) , //为矩阵内容分配内存
pData(new T[n*n]) //将指向该内存的指针存储起来,
{ this->setDataPtr(pData.get()); } //然后将它的一个副本交给base class
...
private:
boost::scoped_array<T> pData;
};
请记住:
Templates生成多个class和多个函数,所有任何template代码都不该与某个造成膨胀的template参数产生相依关系。
因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations)的具现类型(instantiation types)共享实现码。
(45)、运用成员函数模板接受所有兼容类型
智能指针的转换是一个问题,本条款试着探讨这个问题。
Templatea和泛型编程(Generic Programming)
member function templates(常简称member templates),其作用是为class生成函数:
template<typename T>
class SmartPtr {
public:
template<typename U> //member template
SmartPtr(cosnt SmartPtr<U>& other); //为了生成copy构造函数
...
}
以上代码的意思是,对任何类型T和任何类型U,这里可以根据SmartPtr<U>生成一个SmartPtr<T>——因为SmartPtr <T>有个构造函数接受一个SmartPtr<U>参数。创建的对象时同一个template的不同具现体,有时我们称之为泛化(generalize)copy构造函数。泛化(generalize)copy构造函数支持隐式转换。
template<typename T>
class SmartPtr {
public:
template<typename U>
SmartPtr(cosnt SmartPtr<U>& other) //以other的heldPtr
:heldPtr(other.get()) { ... } //初始化this的heldPtr
T* get() const { return heldPtr; }
...
private:
T* heldPtr; //这个SmartPtr持有的内置(原始)指针
}
上面的类只有“存在某个隐式转换可将一个U*指针转为T*指针”时才能通过编译。
member function templates(成员函数模板)的效用不限于构造函数,它们常扮演另一个角色的支持赋值。例如TR1的shared_ptr支持所有“来自兼容之内置指针、tr1::shared_ptrs、auto_ptrs和tr1::weak_ptrs”的构造行为,以及所有来自上述各物(tr1::weak_ptrs除外)的赋值操作。下面是tr1::shared_ptrs的一份摘录:
template<class T>
class shared_ptr {
public:
template<class Y>
explicit shared_ptr(Y* p); //构造,来自任何兼容的内置指针
template<class Y>
shared_ptr(shared_ptr<Y> const& r); //或shared_ptr
template<class Y>
shared_ptr(weak_ptr<Y> const& r); //或weak_ptr
template<class Y>
shared_ptr(auto_ptr<Y> const& r); //或auto_ptr
template<class Y> //赋值,来自任何兼容的shared_ptr或auto_ptr
shared_ptr& operator=(shared_ptr<Y> const& r);
template<class Y>
shared_ptr& operator=(auto_ptr<Y> const& r);
...
}
member template并不改变语言规则,所有如果你想要控制copy构造的方方面面,你必须同时声明泛化copy构造函数和“正常的”copy构造函数,相同规则也适用于赋值(assignment)操作。看如下tr1::shared_ptr的一份定义摘要:
template<class T>
class shared_ptr {
public:
shared_ptr(shared_ptr const& r); //copy构造函数
template<class Y> //泛化copy构造函数
shared_ptr(shared_ptr<Y> const& r);
shared_ptr& operator=(shared_ptr const& r); //copy assignment
template<class Y> //泛化copy assignment
shared_ptr& operator=(shared_ptr<Y> const& r);
...
};
请记住:
请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。
如果你声明member template用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。
(46)、需要类型转换时请为模板定义非成员函数
条款24讨论过唯有non-member函数才有能力“在所有实参身上实施隐式类型转换”,但条款并以Rational class的operator*函数为例。本条款将Rational和operator*模板化了:
template<typename T>
class Rational {
public:
Rational(const T& numerator = 0,const T& denominator = 1); //条款20告诉你为什么参数一以passed
const T numerator() const; // by reference方式传递
const T denominator() const; //条款28告诉你为什么返回值以passed by value方式传递
... //条款3告诉你为什么它们是const
};
template<typename T>
const Rational<T> operator*(cosnt Rational<T>& lhs,const Rational<T>& rhs)
{ ... }
现在来看看实例是否可以运行:
Rational<int> oneHalf(1,2); //这个例子来自条款24,唯一不同是Rational改为template。
Rational<int> result = oneHalf*2; //错误!无法通过编译。
上述失败给我们的启示是,模板化的Rational内的某些东西似乎和其non-template版本不同。事实的确如此。在条款24内,编译器知道我们尝试调用什么函数(就是接受两个Rationals参数的那个operator*啦),但这里编译器不知道我们想要调用那个函数。取而代之的是,他们试图想出什么函数被名为operator*的template具现化(产生)出来。它们知道它们应该可以具现化某个“名为operator*并接受两个Rational<T>参数”的函数,但完成这一具现化行为,必须先算出T是什么。问题是它们没有这个能耐。
因为template实参推导过程中并不考虑采纳“通过构造函数而发生的”隐式类型转换。因此由实例中的2(int)无法推导出Rational<Int>,所有实例无法通过编译。
template class内的friend声明式可以指涉某个特定函数。那意味class Rational<T>可以声明operator*是它的一个friend函数。Class templates并不依赖template实参推导(后者只施行于function templates身上),所有编译器总是能够在class Rational<T>具现化时得知T。因此,令Rational<T> class声明适当的operator*为其friend函数,可简化整个问题:
template<typename T>
class Rational {
public:
...
friend const Rational<T> operator*(cosnt Rational<T>& lhs,const Rational<T>& rhs);
//声明operator*函数
};
template<typename T> //定义operator*函数
const Rational<T> operator*(cosnt Rational<T>& lhs,const Rational<T>& rhs)
{ ... }
现在对operator*的混合式调用可以通过编译了,因为当对象oneHalf声明为一个Rational<int>,class Rational<int>于是被具现化出来,而作为过程的一部分,friend函数operator*(接受Rational<int>参数)也就被自动声明出来。后者身为一个函数而非函数模板(function template),因此编译器可在调用它是使用隐式转换函数(例如Rational的non-explicit构造函数),而这便是混合式调用之所有成功的原因。
但是虽然这段代码通过编译,却无法连接。
先来看看Rational内声明的operator*语法。在声明式一般都省略类型,例如Rational<T>写成Rational。
上面的代码无法连接,因为我们没有提供定义式,最简单可行的办法是将operator*函数本体合并至其声明式内:
template<typename T>
class Rational {
public:
...
friend const Rational<T> operator*(cosnt Rational<T>& lhs,const Rational<T>& rhs);
{
return Rational(lhs.numerator()*rhs.numerator(),lhs.denominator()*rhs.denominator());
//实现码与条款24同
}
};
本来是使用了inline方式来实现friend函数。如果代码比较多可以考虑使用“令friend函数调用辅助函数”的做法。如下:
template<typename T> class Rational; //声明Rational template
template<typename T>
const Rational<T> doMultiply(cosnt Rational<T>& lhs,cosnt Rational<T>& rhs);
template<typename T>
class Rational {
public:
...
friend const Rational<T> operator*(cosnt Rational<T>& lhs,const Rational<T>& rhs);
{
return doMultiply(lhs,rhs);
}
};
请记住:
当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。
(47)、请使用traits classes表现类型信息
STL迭代器分类:
input迭代器:只能向前移动,一次一步,客户只可读取(不能涂写)它们所指的东西,而且只能读取一次。它们模仿指向输入文件的阅读指针(read pointer);C++程序库中的istream_iterators是这一分类的代表。Output迭代器情况类似,但一切只为输出:它们只向前移动,一次一步,客户只可涂写它们所指的东西,而且只能涂写一次。它们模仿指向输出文件的涂写指针(write pointer);ostream_iterators是这一分类的代表。这是威力最小的两个迭代器分类。由于这两类都只能向前移动,而且只能读或写其所指物最多一次,所有它们之适合“一次性操作算法”(one-pass-algorithns)。
forward迭代器可以做前述两种分类所能做的每一件事,而且可以读或写其所指物一次以上。这使得它们可施行于多次性操作算法(mulit-pass-algorithns)。slist迭代器就是这类迭代器。
Bidirectional迭代器除了可以向其移动,还可以向后移动。list迭代器就是这类迭代器,set,multiset,map和multimap的迭代器也都是这一分类。
random access迭代器比Bidirectional迭代器更多的功能是可以执行“迭代器算术”,也就是可以在常量时间内向前或向后跳跃任意距离。vector,deque和string提供的迭代器就是这一类。
C++标准程序库提供的各种迭代器结构如下:
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 {};
现在看STL的adance的实现:
template<tyoename IterT,typename DistT>
void advance(IterT& iter,DistT d)
{
if(iter is a random access iterator)
{
iter += d; //针对random access迭代器使用迭代器算术运算
}
else
{
if(d>=0) { while(d--) ++iter; } //针对其他迭代器分类反复调用++或--
else { while(d++) --iter; }
}
}
Traits并不是C++关键字或一个预先定义好的构件;它们是一种技术,也是一个C++程序员共同遵守的协议。这个技术的要求之一是,它对内置(built-in)类型和用户自定义类型(user-defined)的表现必须一样好。标准技术是把它放进一个template及其一或多个特化版本中。这样的templates在标准程序库中有若干 个,其中针对迭代器被命名为iterator_traits。
template<typename IterT> //template,用来处理迭代器分类的相关信息
struct iterator_traits;
iterator_traits是个struct,习惯上成为trait classes。
iterator_traits的运作方式是,针对每一个类型IterT,在struct iterator_traits<IterT>内一定声明某个typedef名为iterator_category。这个typedef用来确认IterT的迭代器分类。
例如,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;
...
};
...
};
iterator_traits实现如下:
template<typename IterT>
struct iterator_traits {
typedef typename IterT::iterator_category iterator_category;
...
};
针对指针类型需要偏特化:
template<typename IterT>
struct iterator_traits<IterT*> { //template偏特化针对内置指针
typedef typename randon_access_iteraotr iterator_category;
...
};
定义一个traits class的步骤如下:
确认若干你希望将来可取得的类型的相关信息。例如对迭代器而言,我们希望将来可取的其分类(category)。
为该信息选择一个名词(例如iteraotor_category)
提供一个template和一组特化版本(例如iterator_traits),内含你希望支持的类型相关信息。
现在advance实践先前的伪码(pseudocode):
template<tyoename 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类型在编译期间获知,iterator_traits<IterT>::iterator_category可以在编译期间确定。但是if语句却是在运行期才会核定。将编译期间做的事情延迟到运行期才做,不仅浪费时间,也造成可执行文件膨胀。
我们可以使用重载来替代if表达式。
tempalte<typename IterT,typename DistT>
void doAdvance(IterT& iter,DistT d,std::random_access_iterator_tag) //这份实现用于random access迭代器
{
iter += d;
}
tempalte<typename IterT,typename DistT>
void doAdvance(IterT& iter,DistT d,std::bidirectional_iterator_tag) //这份实现用于rbidirectional迭代器
{
if(d>=0) { while(d--) ++iter; }
else { while(d++) --iter; }
}
tempalte<typename IterT,typename DistT>
void doAdvance(IterT& iter,DistT d,std::input_iterator_tag) //这份实现用于input迭代器
{
if(d<0) { throw std::out_of_range("Negative distance"); }
else { while(d++) --iter; }
}
doAdvance的input_iterator_tag版本也能够出来forward迭代器。这是public继承带来的部分好处。
现在可以实现advance的重载调用:
tempalte<typename IterT,typename DistT>
void doAdvance(IterT& iter,DistT d)
{
doAdvance( //调用doAdvance版本
iter,d, //对iter之迭代器分类而言必须是适当的
typename std::iterator_traits<IterT>::iterator_category());
}
现在可以总结如何使用一个traits class了:
建立一组重载函数(身份像劳工)或函数模板(例如doAdvance),彼此间的差异只在于各自的traits参数。令每个函数实现码与其接受之traits信息相应和。
建立一个控制函数(身份像工头)或函数模板(例如advance),它调用上述那些“劳工函数”并传递traits class所提供的信息。
请记住:
Traits classes使得“类型相关信息”在编译期可用。它们以templates和“templates特化”完成实现。
整合重载技术(overloading)后,traits classes有可能在编译期对类型执行if...else测试。
(48)、认识template元编程
所谓template metaprogram(模板元程序)是以C++写成、执行于C++编译器内的程序。一旦TMP程序结束执行,其输出,也就是从template具现出来的若干C++源码,便会一如往常地被编译。
TMP让某些事情更容易。由于TMP执行于C++编译期,因此可将工作从运行期转移到编译期。上一条款的traits解法就是TMP。
st::list<int>::iterator iter;
...
advance(iter,10); //移动iter向前走10个元素,运行期advance无法通过编译
template<tyoename IterT,typename DistT>
void advance(IterT& iter,DistT d)
{
if(typeid(typename std::iterator_traits<IterT>::iterator_category)== typeid(std::random_access_iterator_tag))
{
iter += d; //错误!
}
else
{
if(d>=0) { while(d--) ++iter; } //针对其他迭代器分类反复调用++或--
else { while(d++) --iter; }
}
}
typeid这行会因为list<int>::iterators而失败,但编译器必须确保所有源码都有效,纵使是不会执行起来的代码!而当iter不上random acess迭代器时"iter += d"无效。
TMP强大到足以计算任何事物。
TMP是个“函数式语言”(functional language)。TMP循环一般是通过递归实现,TMP循环并不涉及递归函数调用,而是涉及“递归模板具现化”(recursive template instantiation)。
下面看看TMP如何计算阶乘(factorial):
template<unsigned n> //一般情况:Factorial<n>的值是n乘以Factorial<n-1>的值。
struct Factorial {
enum { value = n * Factorial<n-1>::value };
};
template<> //特殊情况
struct Factorial<0> { //Factorial<0>的值是1
enum { value = 1 };
};
使用Factorial如下:
int main()
{
std::cout << Factorial<5>::value; //印出120
std::cout <<Factorial<10>::value; //印出3628800
}
TMP值得学习。可以看下面三个例子:
确保量度单位正确。
优化矩阵计算。
可以生成客户定制之设计模式(custom design pattern)实现品。
请记住:
Template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
TMP可被用来生成“基于政策选择组合”(base on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。