七 模板与泛型编程
条款41:了解隐式接口和编译期多态
面向对象编程世界总是以显示接口(explicit interface)和运行期多态(runtime polymorphism)解决问题。
class Widget{
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
virtual swap(Widget& other);
};
void doProcessing(Widget& w)
{
if (w.size() > 10 && w != someNastyWidget){
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
说说doProcessing的w:
由于w被声明为Widget,所以w必须支持Widget接口。我们可以在源码中找到这个接口(例如在widget.h中),看看它是什么样子,所以我们称此为一个显式接口,也就是它在源码中明确可见。
Widget某些成员函数是virtual,w对那些函数的调用表现出运行期多态,运行期根据w的动态类型决定究竟调用哪个函数。
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的类型T必须支持size,normalize和swap成员函数,copy构造函数、不等比较!=,并非完全正确。这一组表达式便是T必须支持的一组隐式接口。
凡涉及w的任何调用,例如operator!=,有可能造成template的具现化,是这些调用得以成功。这样的具现化发生在编译期。“以不同的template参数具现化function template”
会导致调用不同的参数,这便是编译期多态(compile-time polymorphism)。类似于“哪个重载函数被调用”(发生在编译期)和“哪一个virtual函数被调用”(发生在运行期)之间的差异。显式接口由函数的签名式(名称、参数类型、返回类型)构成:例如
class Widget{
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
virtual swap(Widget& other);
};
除了上面的还有编译期产生的copy构造函数和copy assignment操作符。另外也可以包括typedefs等。
隐式接口不基于函数签名式,而是由有效表达式(valid expressions)组成。
template<typename T>
void doProcessing(T& w)
{
if (w.size() > 10 && w != someNastyWidget){
T temp(w);
temp.normalize();
temp.swap(w);
}
}
T(w的类型)的隐式接口好像有这些约束:
必须提供一个size成员函数;
必须支持operator!=;
真要感谢操作符重载带来的可能性,这两个约束都不需要满足。
是的,T必须支持size成员函数,然而这个函数可以从base class继承而得。这个函数不需要返回一个整数值,他唯一要做的是返回一个类型为X的对象,而X对象加上一个int(10)必须能够调用一个operator>。这个operator>可以取得类型为Y的参数,只要存在一个隐式转换能将类型X的对象转换为类型Y的对象。
同样道理T并不需要支持operator!=,operator!=只要阶接受一个类型为X和Y的对象,T可以转换为X而someNastyWidget可以转换成Y。
加诸于template参数身上的隐式接口,就像加诸于class对象身上的显式接口一样真实,都在编译期完成检查。
对classes而言,接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生在运行期。
对template参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生在编译期。
请记住:
1.class和template都支持接口和多态。
2.对class而言接口是显示的,以函数签名为中心。多态则是通过virtual函数发生于运行期。
3.对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化合函数重载解析发生于编译器。
条款42:了解typename的双重意义
template声明式中,class和typename这两个关键字意义完全相同
template<class T> class Widget;
template<typename T> class Widget;
有时候你一定要用typename,
可以在template中指涉的两种名称:
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的类型是C::const_iterator 实际上是什么必须取决于template参数C。template内出现的名称如果相依于某个template参数,称之为从属名称(dependent names)。如果从属名称在class内呈嵌套状,称之为嵌套从属名称(nested dependent name)。C::const_iterator 就是这样一个名称嵌套从属名称。
value类型int。不依赖任何template参数的名称。称为非从属名称(non-dependent name)。
嵌套从属名称可能导致解析的困难:
template <typename C>
void print2nd(const C& container)
{
C::const_iterator* x;
}
看起来我们好像声明一个local变量,是个指针,指向一个C::const_iterator。 但它之所以被那么认为,是因为我们“已经知道”C::const_iterator 是个类型。如果
C::const_iterator 不是个类型呢?如果C有个static成员变量碰巧被命名为const_iterator。过时x碰巧是个global变量名称,那样上述代码就是一个相乘动作,
C::const_iterator 乘以x。撰写c++解析器的人必须操心所有可能的输入。
在我们知道C以前,没有任何办法可以知道C::const_iterator 是否为一个类型。而当编译器开始解析template print2nd时,尚未确定C是什么东西。
c++有个规则可以解析此一歧义状态:如果解析器在template中遭遇一个嵌套从属名称,它便假设这个名称不是个类型,除非你告诉它是。缺省情况下从属名称不是类型。此外还有个例外。
所以上述代码不是有效的c++代码。我们必须告诉c++说C::const_iterator 是个类型。只要紧邻它之前放置关键字typename即可:
template <typename C>//这个合法的c++代码
void print2nd(const C& container)
{
if (container.size() >= 2)
{
typename C::const_iterator iter(container.begin());
++iter;
int value = *iter;
std::cout << value;
}
}
typename只用来验明嵌套从属类型名称;其他名称不该有它存在。
template <typename C>
void f(const C& container, //不允许使用typename
typename C::iterator iter);//一定要使用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:
explicit Derived(int x)
:Base<T>::Nested(x)//mem.init.list中不允许“typename”
{
typename Base<T>::Nested temp;//嵌套从属类型既不在base class list中也不在mem.init.list中,
} //作为一个base class修饰符需加上typename
};
让我们看一个typename例子:一个function template,他接受一个迭代器,而我们打算为该迭代器指涉的对象做一份复件temp:
template <typename IterT>
void workWithIterator(IterT)
{
typename std::iterator_traits<IterT>::value_type temp(*iter);
}
这是个标准trait class的一种运用(条款47),相当于说“类型IterT之对象所指之物的类型”。如果IterT是vector<int>::iterator,temp的类型就是int,如果IterT是
list<string>::iterator,temp的类型就是string。由于std::iterator_traits<IterT>::value_type是个嵌套从属类型名称(value_type被嵌套于iterator_traits<IterT>之内而
IterT是个template参数),所以必须在它之前放置typename。
这么长你肯定会想建立一个typedef。对于traits成员名称如value_type,普遍习惯是设定typedef名称用以代表某个traits成员名称:
template <typename IterT>
void workWithIterator(IterT)
{
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
}
请记住:
1.声明template参数时,前缀关键字class和typename可互换。
2.请使用关键字typename标识嵌套从属类型名称;但不得在base class list(基类列表)或member initialization list(成员初值列表)内以它作为base class修饰符。
条款43:学习处理模板化基类内的名称
我们需要一个程序,传送信息到不同的公司去。信息要不译成密码,要不就是未加工的文字。如果编译期间我们有足够信息来决定哪一个信息传至那一家公司,就可以采用基于
template的解法:
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)
{
...;//调用c.sendEncrypted,类似sendClear
}
};
这个做法行的通。但假设我们有时候想要在每次发送出信息的时候志记(log)某些信息。derived class可以轻易加上这样的行为,那似乎是个合情理的解法:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
void sendClearMsg(const MsgInfo& info)
{
将传送前信息写至log;
sendClear(info);
}
};
sendClearMsg 避免遮掩“继承而得的名称”(条款33),避免重新定义一个继承而得的non-virtual函数(条款36)。但上述代码无法通过编译,编译器看不到sendClear。为什么?
问题在于,编译器遇到class template LoggingMsgSender定义式时,并不知道它继承什么样的class。因为MsgSender<Company>中的Company是个template参数,不到后来(当LoggingMsgSender被具现化)无法确切知道它是什么。而如果不知道Company是什么,就无法知道class MsgSender<Company>看起来是个什么样----更明确的说是没办法知道它是否有个sendClear函数。
为了让问题具体化,假设有个class CompanyZ只是用加密通信:
class CompanyZ{
public:
void sendEncrypted(const std::string& msg);
};
一般性的MsgSender template对CompanyZ并不合适,因为那个template提供了一个sendClear函数(其中针对其类型参数Company调用了sendCleartext函数),而这对CompanyZ对象并不合理。与纠正这个问题,我们可以针对CompanyZ产生一个MsgSender特化版;
template<> //一个全特化的
class MsgSender<CompanyZ>{ //MsgSender;它和一般template相同
public: //差别只在于它删掉了sendClear
void sendSecret(const MsgInfo& info)
{
...
}
};
注意class定义式最前头“template<>”语法象征这既不是template也不是标准class,而是个特化版的MsgSender template,在template实参是CompanyZ时被使用。这事模板全特化(total template specialization):template MsgSender针对类型CompanyZ特化了,而且其特化是全面性的,也就是说一旦类型参数被定为CompanyZ,再没有其他template参数可供变化。
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
void sendClearMsg(const MsgInfo& info)
{
将传送前信息写至log;
sendClear(info);//如果Company==CompanyZ,这个函数就不存在
}
};
那就是为什么C++拒绝这个调用的原因:它知道base class template可能被特化,而那个特化版本可能不提供和一般属性template相同的接口。因此它往往拒绝在templatized
base class(模板化基类,MsgSender<Company>)内寻找继承而来的名称(本例的SendClear)。从Object Oriented C++跨进Template c++继承就不想以前那般畅通无阻了。
我们必须令c++“进入templatized base classes观察”。有三个办法:
第一个办法是base class函数调用动作之前加上“this->”:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
void sendClearMsg(const MsgInfo& info)
{
将传送前信息写至log;
this->sendClear(info); //成立,假设sendClear将被继承
}
};
第二个办法是使用using 声明式:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
using MsgSender<Company>::sendClear; //告诉编译器,请他假设sendClear位于base class内
void sendClearMsg(const MsgInfo& info)
{
将传送前信息写至log;
sendClear(info); //成立,假设sendClear将被继承
}
};
这里的using声明式不是条款33中“base class名称被derived class名称遮掩”,而是编译器不进人base class作用域查找,于是我们通过using告诉它,请他这么做。
第三个做法是,明白指出被调用的函数位于base class内:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company>{
public:
void sendClearMsg(const MsgInfo& info)
{
将传送前信息写至log;
MsgSender<Company>::sendClear(info); //成立,假设sendClear将被继承
}
};
但这往往不是令人满意的一个解法,因为如果被调用的是virtual函数,上述的明确资格修饰MsgSender<Company>::会关闭virtual绑定行为。
从名称可视点的角度出发,上述每个解法做的事情都相同:对编译器承诺“base class template的任何特化版本都将支持其一般化版本所提供的接口”。这样一个承诺是编译器在
解析(parse)像LoggingMsgSender这样的derived class template时需要的。但如果这个承诺最终未被实践出来,往后的编译器最终还是会给事实一个公道。例如,如果稍后的源码内含这个:
LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
zMsgSender.sendClearMsg(msgData); //错误!无法通过编译。
因为在那个点上,编译器知道base class是个template特化版本MsgSender<CompanyZ>,而它们知道那个class不提供sendClear函数,而这个函数却是sendClearMsg尝试调用的函数。根本而言,面对“指涉base class members”之无效的references,编译器的诊断时间可能发生在早期(当解析derived class template的定义式时),也可能发生在晚期(当那些templates被特定之template实参具现化时)。C++的政策是宁愿早诊断。这就是为什么“当base classes从templates中被具现化时”它假设它对那base classes的内容毫无所悉的缘故。
请记住:
可在derived class template内通过“this->”指涉base class template内的成员名称,或藉由一个明白写出的“base class资格修饰符”完成。
条款 44:将与参数无关的代码抽离templates
自从我们进入模板编程的第一天,我们就意识到template不仅大大地减少了代码的执行时间,而且能够避免代码潜在的重复.我们不在需要为20个相似的classes而每一个都带有15个成员函数编写独立的一份代码去分别实现,我们只需要键入一个class template,留给编译器去具现化那20个你需要的相关classes和300个函数.如此神奇的技术是不是令你感到很兴奋.是的,这个对每一个软件开发人员来说无疑是一个重大的编码优化手段,然而有时候,如果你不小心,使用templates可能会导致代码膨胀:其二进制码带着重复(或几乎重复)的代码、数据,或两者.
在你编码的时候,通常你有一个强大的工具.那就是:共性与变性分析.其概念从字面上我们就可以了解,即使你从未写过一个template,你始终在进行着这样的分析.当你在写两个函数的时候,你发现它们之间有着相同的部分,此时的你惯性般地将他们之间"公共部分"提出来,作为第三个函数,然后让前两个函数都调用这个第三个函数;同样的道理,当你编写某个class时候,你发现其某些部分与另外一个class的某些部分相同,你也不会去重复该相同部分,而是把相同部分搬移到新的class去,然后在使用复合或继承,令原classes取用这些共同特性.而原classes的特异部分则保持不动. 好,我们将这种分析方法推而广之,它对template同样适用.但在template代码中,重复是隐晦的:毕竟只存在一份template源码,所以你编码时必须万分小心去防止template具现化多次时可能造成的重复.
举个例子,假设现在你要为固定尺寸的矩阵编写一个template类,该类声明要支持矩阵的逆运算.你开始了你的代码:
template <typename T, std::size_t n> //矩阵元素类型T,尺寸大小为n
class SquareMatrix{
public:
...
void invert(); //逆运算
};
写出这样代码很自然,接下来我们考虑如何使用这个template:
SquareMatrix<double,5> square1;
...
square1.invert(); //调用 SquareMatrix<double,5>::invert
SquareMatrix<double,10> square2;
...
square2.invert(); //调用 SquareMatrix<double,10>::invert
这会发生什么事情?这些函数并非完完全全相同.但除了常量5和10,这两个函数的其它部分完全相同.这就是template引出代码膨胀的一个典型例子.
如果你的代码中遇到这样的情况,你会怎么去"亡羊补牢".你的本能会让你为它们建立一个带数值参数的函数,然后用5和10来调用这个带参函数,这样就不会导致代码重复了.好想法不错,我们来实现你的idea.
template <typename T>
class SquareMatrixBase{
protected:
...
void invert(std::size_t matrixSize);//以尺寸大小作为参数的求逆矩阵函数
...
};
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); }
};
这里请注意SquareMatrix和SquareMatrixBase之间的继承关系是private.这反应一个事实:这里的base class只是为了帮助derived class实现,而不是为了表现SquareMatrix与SquareMatrixBase之间是一个is-a关系(关于private继承,见条款39).
到目前为止,情况还令人满意.不过,现在有一个值得我们去思考的问题出现了:SquareMatrixBase::invert如何知道该操作什么数据?虽然它知道了矩阵的尺寸,但怎么能够知道那个矩阵的数据放在什么地方了倪?这里的一个可行的方法是令SquareMatrixBase储存一个指针,该指针指向矩阵数值所在的内存.那我们要操作的东西就有了.哈哈,that's perfect!成果貌似是这样滴:
template <typename T>
class SquareMatrixBase{
protected:
SquareMatrixBase(std::size_t n, T* memory)
:size_(n),data_(memory){}
void setData(T* data){ data_ = data; }
...
private:
std::size_t size_; //矩阵大小
T* data_; //矩阵数据
};
template <typename T, std::size_t n>
class SquareMatrix:private SquareMatrixBase<T>{
public:
SquareMatrix()
:SquareMatrixBase<T>(n,data){} //将矩阵大小和数据指针给base class.
...
private:
T data_[n*n];
};
以上实现当中,可能造成对象自身非常大.于是我们可以将矩阵数据放进heap中:
template <typename T,std::size_t n>
class SquareMatrix:private SquareMatrixBase<T>{
public:
SquareMatrix()
:SquareMatrixBase<T>(n,0),data_(new T[n*n]){
this->setData(data_.get()); //使得base class获得矩阵数据
}
...
private:
boost::scoped_array[T] data_;
};
喔喔,这样的解决方案是不是很棒?是的,很棒,但必须付出代价.带有尺寸模板参数的那个invert版本,可能生成比共享版本(就是以函数参数传递尺寸或存储在对象内)更佳的代码.比如在尺寸专属版本中,尺寸是个编译期常量,因此可以籍由常量的广传达到最优化,包括把它们折进被生成指令中称为直接操作数.这在"与尺寸无关"的版本中是无法办到的.而另一个方面,不同大小的矩阵只拥有单一版本的invert,可减少执行文件大小,也就因此降低程序进程所使用的那一组内存页大小,并强化指令高速缓存区内的引用集中化.这些都可能使得程序执行的更加快速,超越"尺寸专属版"invert的最优化效果. 那哪一个影响占主要地位?要想知道这个答案,唯有让两者尝试并观察你的平台行为以及面对代表性数据组时的行
为.
另一个效能评比所关心的主题就是对象大小.这里我不再详细表述了,大家自己可以分析一下.
本条款只讨论由non-type template parameter(非类型模板参数)带来的膨胀,其实type parameter(类型参数)会导致膨胀.具体解释请看原书作者的表述,限于篇幅的问题,在这里我就不累赘表述了.
请记住:
Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系.
因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数.
因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码.
条款45:运用成员函数模板接受所有兼容类型
smart pointers(智能指针)是行为很像指针但是增加了指针没有提供的功能的 objects。例如,《C++箴言:使用对象管理资源》阐述了标准 auto_ptr 和 tr1::shared_ptr 是
怎样被应用于在恰当的时间自动删除的 heap-based resources(基于堆的资源)的。STL containers 内的 iterators(迭代器)几乎始终是 smart pointers(智能指针);你绝
对不能指望用 "++" 将一个 built-in pointer(内建指针)从一个 linked list(线性链表)的一个节点移动到下一个,但是 list::iterators 可以做到。
real pointers(真正的指针)做得很好的一件事是支持 implicit conversions(隐式转换)。derived class pointers(派生类指针)隐式转换到 base class pointers(
基类指针),pointers to non-const objects(指向非常量对象的指针)转换到 pointers to const objects(指向常量对象的指针),等等。例如,考虑在一个 three-level hierarchy(三层继承体系)中能发生的一些转换:
class Top { ... };
class Middle: public Top { ... };
class Bottom: public Middle { ... };
Top *pt1 = new Middle; // convert Middle* => Top*
Top *pt2 = new Bottom; // convert Bottom* => Top*
const Top *pct2 = pt1; // convert Top* => const Top*
在 user-defined smart pointer classes(用户定义智能指针类)中模仿这些转换是需要技巧的。我们要让下面的代码能够编译:
template<typename T>
class SmartPtr {
public: // smart pointers are typically
explicit SmartPtr(T *realPtr); // initialized by built-in pointers
...
};
SmartPtr<Top> pt1 = // convert SmartPtr<Middle> =>
SmartPtr<Middle>(new Middle); // SmartPtr<Top>
SmartPtr<Top> pt2 = // convert SmartPtr<Bottom> =>
SmartPtr<Bottom>(new Bottom); // SmartPtr<Top>
SmartPtr<const Top> pct2 = pt1; // convert SmartPtr<Top> =>
// SmartPtr<const Top>
在同一个 template(模板)的不同 instantiations(实例化)之间没有 inherent relationship(继承关系),所以编译器认为 SmartPtr<Middle> 和 SmartPtr<Top> 是完
全不同的 classes,并不比(比方说)vector<float> 和 Widget 的关系更近。为了得到我们想要的在 SmartPtr classes 之间的转换,我们必须显式地为它们编程。
在上面的 smart pointer(智能指针)的示例代码中,每一个语句创建一个新的 smart pointer object(智能指针对象),所以现在我们就集中于我们如何写 smart pointer
constructors(智能指针的构造函数),让它以我们想要的方式运转。一个关键的事实是我们无法写出我们需要的全部 constructors(构造函数)。在上面的 hierarchy(继承体
系)中,我们能从一个 SmartPtr<Middle> 或一个 SmartPtr<Bottom> 构造出一个 SmartPtr<Top>,但是如果将来这个 hierarchy(继承体系)被扩充,SmartPtr<Top> objects
还必须能从其它 smart pointer types(智能指针类型)构造出来。例如,如果我们后来加入
class BelowBottom: public Bottom { ... };
我们就需要支持从 SmartPtr<BelowBottom> objects 到 SmartPtr<Top> objects 的创建,而且我们当然不希望为了做到这一点而必须改变 SmartPtr template。
大体上,我们需要的 constructors(构造函数)的数量是无限的。因为一个 template(模板)能被实例化而产生无数个函数,所以好像我们不需要为 SmartPtr 提供一个
constructor function(构造函数函数),我们需要一个 constructor template(构造函数模板)。这样的 templates(模板)是 member function templates(成员函数模板)
(常常被恰如其分地称为 member templates(成员模板))——生成一个 class 的 member functions(成员函数)的 templates(模板)的范例:
template<typename T>
class SmartPtr {
public:
template<typename U> // member template
SmartPtr(const SmartPtr<U>& other); // for a "generalized
... // copy constructor"
};
这就是说对于每一种类型 T 和每一种类型 U,都能从一个 SmartPtr<U> 创建出一个 SmartPtr<T>,因为 SmartPtr<T> 有一个取得一个 SmartPtr<U> 参数的 constructor(
构造函数)。像这样的 constructor(构造函数)——从一个类型是同一个 template(模板)的不同实例化的 object 创建另一个 object 的 constructor(构造函数)(例如,
从一个 SmartPtr<U> 创建一个 SmartPtr<T>)——有时被称为 generalized copy constructors(泛型化拷贝构造函数)。
上面的 generalized copy constructor(泛型化拷贝构造函数)没有被声明为 explicit(显式)的。这是故意为之的。built-in pointer types(内建指针类型)之间的类
型转换(例如,从派生类指针到基类指针)是隐式的和不需要 cast(强制转型)的,所以让 smart pointers(智能指针)模仿这一行为是合理的。在 templatized constructor
(模板化构造函数)中省略 explicit 正好做到这一点。
作为声明,SmartPtr 的 generalized copy constructor(泛型化拷贝构造函数)提供的东西比我们想要的还多。是的,我们需要能够从一个 SmartPtr<Bottom> 创建一个
SmartPtr<Top>,但是我们不需要能够从一个 SmartPtr<Top> 创建一个 SmartPtr<Bottom>,这就像颠倒 public inheritance(公有继承)的含义(参见《C++箴言:确保公开继承
模拟“is-a”》)。我们也不需要能够从一个 SmartPtr<double> 创建一个 SmartPtr<int>,因为这和从 int* 到 double* 的 implicit conversion(隐式转换)是不相称的。我
们必须设法过滤从这个 member template(成员模板)生成的 member functions(成员函数)的群体。
假如 SmartPtr 跟随 auto_ptr 和 tr1::shared_ptr 的脚步,提供一个返回被这个 smart pointer(智能指针)持有的 built-in pointer(内建指针)的拷贝的 get member
function(get 成员函数)(参见《C++箴言:在资源管理类中准备访问裸资源》),我们可以用 constructor template(构造函数模板)的实现将转换限定在我们想要的范围:
template<typename T>
class SmartPtr {
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other) // initialize this held ptr
: heldPtr(other.get()) { ... } // with other's held ptr
T* get() const { return heldPtr; }
...
private: // built-in pointer held
T *heldPtr; // by the SmartPtr
};
我们通过 member initialization list(成员初始化列表),用 SmartPtr<U> 持有的类型为 U* 的指针初始化 SmartPtr<T> 的类型为 T* 的 data member(数据成员)。这
只有在“存在一个从一个 U* 指针到一个 T* 指针的 implicit conversion(隐式转换)”的条件下才能编译,而这正是我们想要的。最终的效果就是 SmartPtr<T> 现在有一个
generalized copy constructor(泛型化拷贝构造函数),它只有在传入一个 compatible type(兼容类型)的参数时才能编译。
member function templates(成员函数模板)的用途并不限于 constructors(构造函数)。它们的另一个常见的任务是用于支持 assignment(赋值)。例如,TR1 的
shared_ptr(再次参见《C++箴言:使用对象管理资源》)支持从所有兼容的 built-in pointers(内建指针),tr1::shared_ptrs,auto_ptrs 和 tr1::weak_ptrs构造,以及从
除 tr1::weak_ptrs 以外所有这些赋值。这里是从 TR1 规范中摘录出来的一段关于 tr1::shared_ptr 的内容,包括它在声明 template parameters(模板参数)时使用 class 而
不是 typename 的偏好。(就像《C++箴言:理解typename的两个含义》中阐述的,在这里的上下文环境中,它们的含义严格一致。)
template<class T> class shared_ptr {
public:
template<class Y> // construct from
explicit shared_ptr(Y * p); // any compatible
template<class Y> // built-in pointer,
shared_ptr(shared_ptr<Y> const& r); // shared_ptr,
template<class Y> // weak_ptr, or
explicit shared_ptr(weak_ptr<Y> const& r); // auto_ptr
template<class Y>
explicit shared_ptr(auto_ptr<Y>& r);
template<class Y> // assign from
shared_ptr& operator=(shared_ptr<Y> const& r); // any compatible
template<class Y> // shared_ptr or
shared_ptr& operator=(auto_ptr<Y>& r); // auto_ptr
...
};
除了 generalized copy constructor(泛型化拷贝构造函数),所有这些 constructors(构造函数)都是 explicit(显式)的。这就意味着从 shared_ptr 的一种类型到另
一种的 implicit conversion(隐式转换)是被允许的,但是从一个 built-in pointer(内建指针)或其 smart pointer type(智能指针类型)的 implicit conversion(隐式
转换)是不被许可的。(explicit conversion(显式转换)——例如,经由一个 cast(强制转型)——还是可以的。)同样引起注意的是 auto_ptrs 被传送给 tr1::shared_ptr
的 constructors(构造函数)和 assignment operators(赋值操作符)的方式没有被声明为 const,于此对照的是 tr1::shared_ptrs 和 tr1::weak_ptrs 的被传送的方式。这
是 auto_ptrs 被复制时需要独一无二的被改变的事实的一个必然结果(参见《C++箴言:使用对象管理资源》)。
member function templates(成员函数模板)是一个极好的东西,但是它们没有改变这个语言的基本规则。《C++箴言:了解C++偷偷加上和调用了什么》阐述的编译器可以产
生的四个 member functions(成员函数)其中两个是 copy constructor(拷贝构造函数)和 copy assignment operator(拷贝赋值运算符)。tr1::shared_ptr 声明了一个
generalized copy constructor(泛型化拷贝构造函数),而且很明显,当类型 T 和 Y 相同时,generalized copy constructor(泛型化拷贝构造函数)就能被实例化而成为
"normal" copy constructor(“常规”拷贝构造函数)。那么,当一个 tr1::shared_ptr object 从另一个相同类型的 tr1::shared_ptr object 构造时,编译器是为
tr1::shared_ptr 生成一个 copy constructor(拷贝构造函数),还是实例化 generalized copy constructor template(泛型化拷贝构造函数模板)?
就像我说过的,member templates(成员模板)不改变语言规则,而且规则规定如果一个 copy constructor(拷贝构造函数)是必需的而你没有声明,将为你自动生成一个。
在一个 class 中声明一个 generalized copy constructor(泛型化拷贝构造函数)(一个 member template(成员模板))不会阻止编译器生成它们自己的 copy constructor(
拷贝构造函数)(非模板的),所以如果你要全面支配 copy construction(拷贝构造),你必须既声明一个 generalized copy constructor(泛型化拷贝构造函数)又声明一个
"normal" copy constructor(“常规”拷贝构造函数)。这同样适用于 assignment(赋值)。这是从 tr1::shared_ptr 的定义中摘录的一段,可以作为例子:
template<class T> class shared_ptr {
public:
shared_ptr(shared_ptr const& r); // copy constructor
template<class Y> // generalized
shared_ptr(shared_ptr<Y> const& r); // copy constructor
shared_ptr& operator=(shared_ptr const& r); // copy assignment
template<class Y> // generalized
shared_ptr& operator=(shared_ptr<Y> const& r); // copy assignment
...
};
请记住:
使用 member function templates(成员函数模板)生成接受所有兼容类型的函数。
如果你为 generalized copy construction(泛型化拷贝构造)或 generalized assignment(泛型化赋值)声明了 member templates(成员模板),你依然需要声明 normal
copy constructor(常规拷贝构造函数)和 copy assignment operator(拷贝赋值运算符)。
条款46:需要类型转换时请为模板定义非成员函数
1.在类外定义的模板函数,在实参具现化时不进行隐式类型转换:可以在函数调用过程中进行这样的转换,但是在能够调用一个函数之前,编译器必须知道那个函数存在,而为了
知道它,必须先为相关的函数模板具现化参数类型。这是template C++与面向对象的C++不同的地方。
对条款24中的例子进行模板化:
template<typename T>
class Rational{
public:
Rational(const T& numerator = 0, const T& denominator = 1); //传引用
const T numerator() const;
const T denominator() const; //传值,并且是const
...
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs){...} //在类外定义函数重载操作符,条款20有详细说明
Rational<int> oneHalf(1, 2);
Rational<int> result = onehalf * 2; //错误,不能通过编译!无法确知第二个参数的具体类型,因而无法进行隐式类型转换
奇怪,难道换成了模板就不行了?事实确实如此!在条款24中编译器能够知道我们在尝试调用哪个函数(就是接受两个Rationals参数的那个operator*啦),但在这里编译器却不知道我
们要调用哪个函数,它们试图想出什么函数被名为operator*的template具现化出来.但现在的问题是它们没有足够大的能耐来完成如此的工作.为了完成具现化工作,必须先算出T是
什么,于是它们开始了下面的尝试:编译器首先看到了operator*的两个参数,它们的类型分别是Rational<int>(one_half类型)和int(2的类型),每个参数分开考虑.以one_half进行推
导比较容易,operator*的第一个参数声明为Rational<T>,而传递给函数的第一实参类型是Rational<int>,故T一定int.但到了第二个参数推导的时候,问题就来了.operator*声明的
第二个参数为Rational<T>,但实参确实int类型(2).编译器如何推算T?会不会发生像条款24出现的隐式参数转换呢?(编译器适用Rational<int>的non-explicit构造函数将2转换为
Rational<int>,进而将T推导为int),但事实却是很惨酷的:它们没有那样做.因为在template实参推导过程中从不将隐式类型转换函数纳入考虑.这下麻烦了,那有什么办法能够解决
这一问题的呢?别急,其实我们只需要利用一个事实就可以缓和编译器在template实参推导方面受到的挑战,这个事实就是:template class内的friend声明式可以指涉某个特定的
non-member函数.class templates并不依赖template实参推导(后者只施行于function templates身上),所以编译器总是能够在class Rational<T>具现化时得知T.因此,我们的问题
的解决方案就出来了:令Rational<T> class声明适当的operator*为其friend函数.
template<typename T>
class Rational{
public:
friend const Rational operator*(const Rational& lhs, const Rational& rhs); //声明为友元函数,但不在类内定义,从而连接器无法找到该函数定义
...
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) //定义。不能通过连接!
{
...
}
现在对operator*的混合式调用就可以编译了,因为当对象one_half被声明为Rational<int>,class Rational<int>
就被具现化出来了,而作为过程的一部分,friend函数operator*也就被自动声明出来了,此时后者为一个具现的函数而不是函数模板,因此编译器可在调用它时使用隐式转换,于是混
合式通过编译,但链接出错。现在我们回头来思考这个问题.混合式代码通过了编译是因为编译器知道我们要调用哪个函数,但那个函数只被声明与Rational内,并没有被定义出来.
而我们意图令此class外部的operator* template提供定义式,但是行不通--------如果我们自己声明了一个函数,就有责任定义那个函数.既然我们没有提供定义式,连接器当然找不
到它!
最简单的可行方法就是将operator*函数本体合并至其声明式内:
template<typename T>
class Rational{
public:
... //同上
friend const Rational operator*(const Rational& left_handle_side,
const Rational& right_handle_side ){
return Rational( left_handle_side.numerator() * right_handle_side.numerator(),
left_handle_side.denominator() * right_handle_side.denominator() );
}
};
对operator*的混合调用现在可以编译连接并执行了。
这项技术的一个趣点是,我们虽然使用了friend,却与friend的传统用途“访问class的non-public成分”毫不相干。为了让类型转换可能发生于所有实参身上,我们需要一个non
-member函数(条款24);为了令这个函数自动具现化,我们需要将它声明在class内部;而在class内部声明non-member函数的唯一办法就是:令它成为一个friend。因此我们就这
样做了。
operator*定义体放到了类的内部成了inline函数,而inline声明会给类带来冲击.为了最小化这种冲击,我们可以令operator*不做任何事情,只调用一个定义于class外部的辅助函数
,当然,对本条款中的例子而言,这样做没有太大的意义,因为operator*只是一个单行函数,但对于更复杂的函数而言,这样做也许就有价值.本款的例子典型长成这样:
template<typename T> class Rational;
template<typename T>
const Rational<T> doMultiply(const Rational<T>& left_handle_side,
const Rational<T>& right_handle_side);
template<typename T>
class Rational{
public:
...
friend const Rational<T> operator*(const Rational<T>& left_handle_side,
const Rational<T>& right_handle_side){
return doMultiply( left_handle_side, right_handle_side );
}
...
};
template<typename T> // define
const Rational<T> doMultiply(const Rational<T>& lhs, // helper
const Rational<T>& rhs) // template in
{ // header file,
return Rational<T>(lhs.numerator() * rhs.numerator(), // if necessary
lhs.denominator() * rhs.denominator());
}
请记住:
当我们编写一个class template,而它所提供之'于此template相关的'函数支持'所有参数之隐式类型转换'时,请将那些函数为'class template内部的friend函数'.
条款47:请使用traits classes表现类型信息(难)
今天我们讨论的这款内容涉及到一个STL实现上的一个关键技术traits技术,简单的说就是类的型别判定技术.
由于本条款要讨论内容比较多,所以我将分成两篇文章来表述,今天我们只讨论traits classes的实现,明天我将讨
论其使用,废话我就不多说了,我们现在就开始:
我们知道STL迭代器可分为五类,我再来简单的唠叨一下:input迭代器只能向前移动,一次一步,客户只可读取(
不能修改)它们所指的内容,而且只能读取一次;output迭代器情况类似,只是为了输出;以上这两种分别模仿文件的
读写指针,分类的代表为istream_iterators和ostream_iterators.它们两个属于能力最小的迭代器分类,只适合一
次性操作算法.第三类迭代器为forward迭代器,该种迭代器能够做上述两种类所能做的每一件事情,而且可以读写
所指物一次以上.第四类迭代器为bidirectional迭代器,比前一种威力更大,除了可以向前移动还可以向后移动.
STL的list,set,multiset,map,multimap的迭代器都是属于这一分类.最后一种分类也是威力最强的迭代器当属
random access迭代器,它可以在常量时间内向前或向后迢遥任意距离.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算法里面的函数advance的实现,其作用就是将某个迭代器移动某个距离:
template <typename IterT, typename DistT>
void advance(IterT& iter, DistT d);
为了对所有的迭代器分类都进行,我们最希望以下列方式进行实现:
template <typename 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;
}
}
}
当写完上面的伪以后,我们就会立刻将焦点转移到一个点上:必须能够判断iter是否为random access迭代器
,也就是说需要知道类型IterT是否为random access迭代器分类.换句话说我们需要取得类型的某些信息.那就是
traits让你得以进行的事:它们允许你在编译期间取得某些类型信息.
Traits并不是C++关键字或一个预先定义好的构件;它们是一种技术,该技术的要求之一是,它对内置类型和用
户自定义类型的表现必须一样好.这就意味着'类型内的嵌套信息'这种东西出局了,因为我们无法将信息嵌套于
原始指针内.因此类型的traits信息必须位于类型自身之外.标识程序库的做法是把它放进一个template及其一个
或多个特化版本中.这样的templates在标准程序库中有若干个,其中针对迭代器者被命名为iterator_traits:
template<typename IterT>
struct iterator_traits;
iterator_traits以两个部分实现上述所言.首先它约定每一个用户自定义迭代器类型必须嵌套一个typedef,
名为iterator_category,用来确认类型信息.比如deque可随机访问,所以针对deque迭代器而设计的class看起来
会是这个样子:
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只要响应iterator class的嵌套式typedef即可:
template <typename IterT>
struct iterator_traits{
typedef typename IterT::iterator_category iterator_category;
...
};
上面的对用户自定义类型行得通,但是仅仅这样的iterator_traits是不够的,你必须还要提供对指针(也是一种
迭代器)的支持,因为指针不可能嵌套typedef.于是我们就让iterator_traits的第二部分专门对付指针,它所利用的
工具就是模板的偏特化技术.由于指针的行径与random access迭代器类似,所以iterator_traits为指针指定的迭代
器类型如下:
template<typename IterT>
struct iterator_traits<IterT*>{
typedef random_access_iterator_tag iterator_category;
...
};
OK!现在你知道如何来实现一个traits class了,我们来大概简述一下如何设计并实现一个traits class:
■ 确认若干你希望将来可取得的类型信息.
■ 为该信息选一个名称(例如iterator_category)
■ 提供一个template和一组特化版本,内含你希望支持的类型相关信息.
好,我们现在有了iterator_traits,我们可以可以这样来实现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类型在编译期间获知,所以iterator_traits<IterT>::iterator_category也可在编译期间确定.但if语句
确是在运行期才会核定.为什么将可在编译期完成的事情延期到运行期才做呢?这不仅浪费时间,也造成可执行文件膨胀.我
们真正想要的是一个条件式来判断'编译器核定成功'的类型.恰巧C++的重载机制就满足这个需求,哇哈哈!
由于重载函数是根据传来的实参选择最佳的重载体,所以为了让advance的行为如我们所期望,我们需要产生几个接受不
同类型的iterator_category对象作参数的函数,我们将这函数取名为doAdvance:
//这份实现用于random access迭代器
template<typename IterT,typename DistT>
void doAdvance( IterT& iter, DistT d, std::random_access_iterator_tag)
{
iter += d;
}
//这份实现用于bidirectional迭代器
template<typename IterT,typename DistT>
void doAdvance( IterT& iter, DistT d, std::bidirectional_iterator_tag)
{
if( d >= 0 ){
while( d-- ){
++iter;
}
} else {
while( d++ ){
--iter;
}
}
}
//这份实现用于input迭代器
template<typename IterT,typename DistT>
void doAdvance( IterT& iter, DistT d, std::input_iterator_tag)
{
if( d < 0 ){
throw std::out_of_range( "Negative distance" );
}
while( d-- ){
++iter;
}
}
有了这些doAdvance重载版本,advance需要做的只是调用它们并额外传递一个对象,后者必须带有适当的迭代器分
类,于是编译器运用重载机制调用适当的实现代码:
template<typename IterT,typename DistT>
void advance( IterT& iter, DistT& d ){
doAdvance( iter, d, typename std::iterator_traits<IterT>::iterator_category() );
}
现在我们来总结一下如何使用一个traits class了:
■ 建立一组重载函数或模板函数,彼此之间差异只在于各自的traits参数.令每个函数实现码与其接受之traits
信息相应和.
■ 建立一个控制函数或函数模板,它调用上述重载函数并传递traits class所提供的信息.
好了,今天任务完成!
请记住:
Traits classes使得'类型相关信息'在编译期可用.它们以templates和'templates特化'完成实现.
整合重载技术后,traits classes有可能在编译期对类型执行if...else测试.
条款48:认识template元编程(没读懂)
Template metaprogramming(TMPS,模板元编程)是编写template-based C++程序并执行编译期的过程.它本
质上是以C++写成、执行于C++编译器内的程序.一旦TMP程序结束执行,其输出,也就是从templates具现出来的若
干C++源码,便会一如往常地被编译.TMP有两个伟大的效力.第一,它让某些事情更容易.如果没有它,那些事情将
是困难,甚至不可能.第二,欲与TMP执行于C++编译期,因此可将工作从运行期转移到编译期.这导致一个结果是,
某些错误原本通常在运行期才能检测到,现在可在编译期找出来.这样带来的结果是很美好的.因为使用TMP的C++
程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存要求.然而它也会带来另一个令人
感到不愉快的结果是,编译时间变长了.
前面一款我们提到了使用typeid的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 ) ){
iter += d;
} else {
if( d >= 0 ){
while( d-- ){
++iter;
}
} else {
while( d++ ){
--iter;
}
}
}
}
上一条款中我曾提过advance的typeid-based实现方式可能导致编译期问题,我们现在就写出如下代码测试:
std::list<int>::iterator iter;
...
advance( iter, 10 ); //compile error:error C2676: binary '+=' : 'std::list<_Ty>::
//_Iterator<_Secure_validation>' does not define this operator or a conversion to
//a type acceptable to the predefined operator.
上述错误信息显示的是iter += d;由于这行代码导致的.这怎么可能?list<int>::iterator是bidirectional
迭代器,并不支持+=,只有random access迭代器才支持,此刻我们知道编译器绝对不会执行+=那一行代码的,因为
测试typeid那行的if语句总是不成立.你开始郁闷了吧?我在这里要说的是:在用TMP编写的C++程序中,编译器必须
确保所有源码都有效,纵使是不会执行的代码.晕,原来还有这样的啊,其实你仔细想一想,这样做也是可以理解的:
TMP程序被具现化以后发生在编译期,编译器必须依靠执行的具现化代码才能确定哪些语句能执行到,为了对所有可
能的具现化代码编译的支持,编译器也只能这样做!葛大爷在他的一部影片中说了一句:'有枣没枣打一杆,宁可错杀
一千,不愿放走一个.',要是用在这里的话也有一定的道理,呵呵.
为求领悟TMP之所以值得学习,很重要一点是先对它能够表达的目标有一个比较好的理解,原书上对下面的每一
个点都举了例子进行阐述,我对这些阐述还没有理解的足够的深刻,所以在这里我也不敢误人子弟地阐述自己的理解
.我只帖出来原书上出现的三个TMP运用的例子,具体的阐述过程,我建议各位还是看原书上的阐述吧!这三个例子为:
■ 确保度量单位正确.
■ 优化矩阵运算.
■ 可以生成客户定制之设计模式实现品.
TMP或许永远不会成为主流,但对某些程序员--特别是程序库开发人员---几乎确定成为他们的主要粮食.
请记住:
TMP可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率.
TMP可别用来生成'基于政策选择组合'的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码.