模板与泛型编程
Rule 41 了解隐式接口和编译期多态
面向对象编程世界总是以显式接口(explicit interfaces)和运行期多态(runtime polymorphism)解决问题。
Templates及泛型编程的世界,与面向对象有根本上的不同。在此世界中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口(implicit interfaces )和编译期多态(compile-time polymorphism)移到前头了。
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好像必须支持。ize, normalize和swap成员函数、copy构造函数(用以建立temp)、不等比较(inequality comparison,用来比较someNasty-Widget).我们很快会看到这并非完全正确,但对目前而言足够真实。重要的是,这一组表达式(对此template而言必须有效编译)便是T必须支持的一组隐式接口(implicit interface)。
-
凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化(instantiated ),使这些调用得以成功。这样的具现行为发生在编译期。“以不同的template参数具现化function templates”会导致调用不同的函数,这便是所谓的编译期多态(compile-time polymorphism )。
在具象化的过程中,必须支持所有的隐式接口,否则编译失败。
-
classes和templates都支持接口(interfaces)和多态(polymorphism )。
-
对classes而言接口是显式的(explicit ),以函数签名为中心。多态则是通过virtual函数发生于运行期。
-
对template参数而言,接口是隐式的(implicit ),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。
Rule 42 了解typename的双重意义
template<class T>class Widget;//使用”Class" template<typename T> class Widget;//使用”typename"
以上两种用法意义完全相同。 然而C++并不总是把class和typename视为等价。有时候你一定得使用typename。为了解其时机,我们必须先谈谈你可以在template内指涉(refer to)的两种名称。
假设我们有个template function,接受一个STL兼容容器为参数,容器内持有的对象可被赋值为ints。
template<typename C> void print2nd(const C& container) //打印容器内的第二元素 {//注意这不是有效的C++代码 if (container.size()>=2){ C::const_iterator iter(container.begin());//取得第一元素的迭代器 ++iter;//将iter移往第二元素 int value = *iter;//将该元素复制到某个int. std::cout<<value; //打印那个int. } }
-
特别强调两个local变量iter和value. iter的类型是C::const_iterator,实际是什么必须取决于template参数C. template内出现的名称如果相依于某个template参数,称之为从属名称(dependent names )。如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称(nested dependent name )。C::const_iterator就是这样一个名称。实际上它还是个嵌套从属类型名称(nested dependent type name ),也就是个嵌套从属名称并且指涉某类型。
-
print2nd内的另一个local变量value,其类型是int。int是一个并不倚赖任的template参数的名称。这样的名称是谓非从属名称(non-dependent names )。
-
嵌套从属名称有可能导致解析(parsing)困难。
template<typename C> void print2nd(const C& container) { C::const_iterator* x; };
以上的C::const_iterator可以解析为C域中的对象,或者static实例与x相乘。所以为了让解析器能够顺利解释出含义。矫正这个形势,我们必须告诉C十十说C::const iterator是个类型。只要紧临它之前放置关键字typename即可:
template<typename C> void print2nd(const C& container) { if (container.size()>=2){ typename C::const_iterator iter(container.begin()); //... } //这是合法的 };
例外情形:typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class修饰符。例如:
template<typename T> class Derived: public Base<T>::Nested{//base class list中 public://不允许’'typename". explicit Derived(int x) :Base<T>::Nested(x) //mem而t. list中 {//不允许”ypename". 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); //名字过长 建议在函数内使用别名 typedef typename std::iterator_traits<IterT>::value_type temp(*iter) value_type;
-
声明template参数时,前缀关键字class和typename可互换。
-
请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。
Rule 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); }; class MsgInfo{ template<typename Company> class MsgSender{ public: //针对其他公司设计的classes. //这个class用来保存信息,以备将来产生信息 //构造函数、析构函数等等。 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; sendClear(info);//调用base class函数:这段码无法通过编译。 //将“传送后”的信息写至log; } };
问题在于,当编译器遭遇class template LoggingMsgSender定义式时,并不知道它继承什么样的class。当然它继承的是MsgSender<Company>,但其中的Company是个template参数,不到后来(当LoggingMsgSender被具现化)无法确切知道它是什么。而如果不知道Company是什么,就无法知道class MsgSender<Company>看起来像什么—更明确地说是没办法知道它是否有个sendClear函数。
也就是子类不会把Base的函数当成是可搜索的空间。
注意以下特例,把sendClear删除。
class CompanyZ{ public: //这个C}BSS不提供 //sendCleartext函数 void sendEncrypted(const std::string& msg); }; template<> class MsgSender<CompanyZ>{ public: //一个全特化的 //MsgSender;它和一般template相同, //差别只在于它删掉了sendClear void sendSecret(const MsgInfo& info) {...} };
现在,MsgSender针对CompanyZ进行了全特化,让我们再次考虑derived class LoggingMsgSender:
template<typename Company> class LoggingMsgSender: public: public MsgSender<Company> void sendClearMsg(const MsgInfo& info) { //将“传送前”的信息写至log; sendClear(info);//如果Company==CompanyZ,这个函数不存在· //将“传送后”的信息写至log; }
因为那个class并未提供sendClear函数!那就是为什么C++拒绝这个调用的原因:它知道base class templates有可能被特化,而那个特化版本可能不提供和一般性template相同的接口‘因此它往往拒绝在templatized base classes(模板化基类,本例的MsgSender<Company>)内寻找继承而来的名称(本例的SendClear)。就某种意义而言,当我们从Object Oriented C++跨进下emplate C++(见条款1),
解决方案:
-
第一是在base class函数调用动作之前加上"this->":
-
第二是使用using声明式。如果你己读过条款33,这个解法应该会令你感到熟悉。条款33描述using声明式如何将“被掩盖的base class名称”带入一个derived class作用域内。
-
明白指出被调用的函数位于base class内.
template<typename Company> class LoggingMsgSender: public MsgSender<Company>{ public: //method 2 using MsgSender<COmpany>::sendClear; //告诉编译器,请它假设 void sendClearMsg(const MsgInfo& info) { // 将“传送前”的信息写至log; //method 1 this->sendClear(info);//成立,假设sendClear //method 3 MsgSender<Company>::sendClear(info); //将“传送后”的信息写至log; } };
以上方法相当于声明,在实际编译中根据具体情况看是否会失败。
-
可在derived class templates内通过”this->',指涉base class templates内的成员名称,或藉由一个明白写出的“base class资格修饰符”完成。
Rule 44 将与参数无关的代码抽离templates
举个例子,假设你想为固定尺寸的正方矩阵编写一个template。该矩阵的性质之一是支持逆矩阵运算(matrix inversion)。
template<typename T,std::size t n> class SquareMatrix{ public: //!!template支持n x n矩阵 //类型为T的objects;见以下 //关于size void invert(); //求逆矩阵 }; SquareMatrix<double,5> sml;//调用SquareMatrix<double,5>::invert sml.invert(); SquareMatrix<doub1e,10> sm2;//调用SquareMatrix<double,l0>::invert sm2.invert();
这样就会具化出两份代码。为了进一步抽象,组织成基类,把invert函数封装进去。
template<typename T> class SquareMatrixBase{ protected: //与尺寸无关的base class. //用于正方矩阵 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;见条款33 public: void invert(){this->rovert(n); //制造一个inline调用,调用 //base class版的invert。稍后 //说明为什么这儿出现this-> };
SquareMatrixBase也是个template,不同的是它只对“矩阵元素对象的类型”参数化,不对矩阵的尺寸参数化。因此对于某给定之元素对象类型,所有矩阵共享同一个(也是唯一一个)SquarMatrixBase class。它们也将因此共享这唯一一个class内的invert.
另一个要考虑的问题是矩阵所处的位置。可以通过基类成员变量,使用指针的方式从子类传到基类。
这个条款只讨论由non-type template parameters(非类型模板参数)带来的膨胀,其实type parameters(类型参数)也会导致膨胀。例如在许多平台上int和long有相同的二进制表述,所以像vector<int>和vector<long>的成员函数有可能完全相同—这正是膨胀的最佳定义。
-
Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
-
因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
-
因类型参数(type parameters )而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations)的具现类型( instantiation types )共享实现码。
Rule 45 运用成员函数模板接受所有兼容类型
真实指针做得很好的一件事是,支持隐式转换(implicit conversions )。Derived class指针可以隐式转换为base class指针。
但如果想在用户自定的智能指针中模拟上述转换,稍稍有点麻烦。我们希望以下代码通过编译:
template<typename T> Class SmartPtr{ public: explicit SrnartPtr(T* realPtr); //智能指针通常 //以内置(原始)指针完成初始化 }; SimartPtr<Top> ptl= SmartPtr<Middle>(new Middle);//将SmartPtr<Middle>转换为 SmnartPtr<Top> SmartPtr<Top> pt2= SmartPtr<Bottom>(new Bottom);//将SYnartPtr<BOttom>转换为 SmartPtr<Top> SmartPtr<const Top> pct2 = pt1; //将SmartPtr<Top>转换为 //SmartPtr<const Top>
Templates和泛型编程(Generic Programming)
为了实行类型转换一个办法就是通过构造函数,但是我们没有办法预期我们的子类。所以必须写一个构造模板。这样的模板(templates)是所谓member function templates(常简称为member templates ),其作用是为class生成函数:
template<typename T> class SmartPtr{ public: template<typename U> SmartPtr(const SmartPtr<U>& other); //member template, //为了生成copy构造函数 };
以上代码的意思是,对任何类型T和任何类型U,这里可以根据SmartPtr<U>生成一个SmartPtr<T>—因为SmartPtr<T>有个构造函数接受一个SmartPtr<U>参数。这一类构造函数根据对象u创建对象t(例如根据SmartPtr<U>创建一个SmartPtr<T>),而u和v的类型是同一个template的不同具现体,有时我们称之为泛化(generalized) copy构造函数。
member function templates(成员函数模板)的效用不限于构造函数,它们常扮演的另一个角色是支持赋值操作。例如TRl的shared——ptr(见条款13)支持所有“来自兼容之内置指针、trl : : shared_ptrs, auto_ptrs和trl : : weak_ptrs(见条款54)”的构造行为,以及所有来自上述各物(trl::weak_ptrs除外)的赋值操作。以下是shared_ptr部分实现。
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); template<class Y> explicit shared_ptr(weak_ptr<Y> const& r) template<class Y> explicit shared_ptr(auto_ptr<Y>& r); template<class Y> //赋值,来自任何兼容的 shared ptr& operator=(shared ptr<Y> const& r);//share电ptr, template<class y> //或auto- ptr. shared_ptr& operator=(auto_ptr<Y>& r);
上述所有构造函数都是explicit,惟有“泛化copy构造函数”除外。那意味从某个shared_ptr类型隐式转换至另一个shared_ptr类型是被允许的,但从某个内置指针或从其他智能指针类型进行隐式转换则不被认可.
-
请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。
-
如果你声明member templates用于泛化copy构造”或‘泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。
Rule 46 需要类型转换时请为模板定义非成员函数
在Rlue 24中已经讨论了为什么只有non-member函数才可以“在所有的实参身上实施隐式类型转换”。那么该条例就是为了将其模板化。
按照之前的写法模板化就变成:
template<typenameT> class Rational{ public: Rational(const T& numerator=0, const T& denominator=1); const T numerator()const; const T denominator()const; }; template<typename T> const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs); Rational<int> oneHalf(1, 2);//这个例子来自条款24} //唯一不同是Rational改为template. Rational<int> result = oneHalf*2;//错误!无法通过编译。
编译失败,编译器找不到我们所需要的函数。因为它推断不出来我们所需要的类型。以oneHalf去推断可以推断出int类型。但是第二个实参是2.期盼的编译器使用Rational<int>的non-explicit构造函数将2转换为Rational<int>,进而将T推导为int,但它们不那么做,因为在template实参推导过程中从不将隐式类型转换函数纳入考虑。
既然不能推断出函数的具体签名,也就不能产生该函数。
接下来, ,作为friend函数,在类具现化的时候把函数同时声明。
template<typename T> class Rational{ public: friend//声明 const Rational operator*(const Rational& lhs,//operator*函数, const Rational& rhs);//细节详下。 }; template<typename T> //定义 const Rational<T> operator*(const Rational<T>& lhs, //operator*函数。 const Rational<T>& rhs){}
以上代码通过编译,但是在连接的时候失败了。因为以上根本没有实现operator*函数。
template<typename T> class Rational{ public: friend const Rational operator*(const Rational& lhs, const Rational& rhs){ return Rational(lhs.numerator()*rhs .numerator(),//实现码与 lhs .denominator()*rhs .denominator());//条款24同 } };
把实现放在类模板中就可以实现这个声明的同时完成定义。
接下来讨论friend函数。friend本意上访问class的not-public部分,这和本意无关。为了让类型转换,又必须是一个non-member函数。为了在class内部声明一个non-member函数(在具现化类的时候产生声明及定义)的唯一办法就是让它成为friend函数。
-
当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数。
Rule 47 请使用traits classes表现类型信息
traits classes是一类tag,需要类定义以及具体的traits配合实现。在这里不做阐述。
-
Traits classes使得“类型相关信息”在编译期可用。它们以templates和“templates特化”完成实现。
-
整合重载技术(overloading)后,traits classes有可能在编译期对类型执行if...else测试。
Rule 48 认识template元编程
TMP有两个伟大的效力。第一,它让某些事情更容易。如果没有它,那些事情将是困难的,甚至不可能的。第二,由于template metaprograms执行于C++编译期,因此可将工作从运行期转移到编译期。这导致的一个结果是,某些错误原本通常在运行期才能侦测到,现在可在编译期找出来。另一个结果是,使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。然而将工作从运行期移转至编译期的另一个结果是,编译时间变长了。是的,程序如果使用TMP,其编译时间可能远长于不使用TMP的对应版本。
-
Template metaprogramming < TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
-
TMP可被用来生成“基于政策选择组合”( based on combinations of policy choices )的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。