模板与泛型编程
C++ templates的最初发展动机很直接:让我们得以建立“类型安全”的容器如vector,list和map。然而当愈多人用上templates,他们发现templates有能力完成愈多可能的变化。容器当然很好,但泛型编程——写出的代码和其所处理的对象类型彼此独立——更好。STL算法如for_each, find 和 merge 就是这一类编程的成果。最终人们发现,C++ template机制自身是一部完整的图灵机:它可以被用来计算任何可计算的值。于是导出了模板元编程,创造出“在C++编译器内执行并于编译完成时停止执行”的程序。容器反倒只成为C++ template 上的一小部分。然而,尽管template 的应用如此宽广,有一组核心观念一直支撑着所有基于template的编程。那些观念便是本章焦点。
条款41 : 了解隐式接口和编译期多态
面向对象编程世界总是以显式接口和运行期动态解决问题。如下代码所示:
class Widget { public: Widget(); virtual ~Widget(); virtual std::size_t size() const; virtual void normalize(); void swap(Widget& other); // 条款25 ..... }; void doProcessing(Widget& w) { if (w.size() > 10 && w != someNastyWidget) { Widget temp(w); temp.normalize(); temp.swap(w); } }
1.由于w的类型被声明为Widget,所以w必须支持Widget接口。我们可以在源码中找出这个接口,看看它是什么样子,所以我们称为一个显式接口,也就是它在源码中明确可见。
2.由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态,也就是说将于运行期根据w的动态类型(条款37)决定究竟调用哪一个函数。
Templates及泛型编程的世界,与面向对象有根本不同。在此世界中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口和编译器多态更显重要了。如下:
template<typename T> void doProcessing(T& w) { if (w.size() > 10 && w != someNastyWidget) { Widget temp(w); temp.normalize(); temp.swap(w); } }
3.w 必须支持哪一种接口,系由template中执行于w身上的操作来决定。本例看来w的类型T好像必须支持size,normalize和swap成员函数,copying函数(用来建立temp),不等比较(用来比较someNasty-Widget)等等。这一组表达式(对此template而言必须有效编译)便是T必须支持的一组隐式接口。
4.凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化,使这些调用得以成功。这样的具现行为发生在编译期。“以不同的template参数具现化function templates(函数模板)”会导致调用不同的函数,这个便是所谓的编译期多态。(“哪一个函数应该被调用”——发生在编译期; “哪一个virtual函数该被绑定”——发生在运行期)。
通常,显式接口由函数签名式(也就是函数名称、参数类型、返回类型)构成。
隐式接口就完全不同了。它并不基于函数签名式,而是由有效表达式组成。
template参数身上的隐式接口,就像class对象身上的显示接口一个样真实,二者都在编译期完成检查。无法在template中使用“不支持template所要求之隐式接口”的对象,代码编译通不过。
请记住:
1. classes 和 templates 都支持接口和多态。
2. 对classes而言接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期。
3. 对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。
条款42 : 了解typename的双重意义
下面代码,从C++ 的角度来看,声明template参数时,class 和 typename 的意义完全相同:
template<class T> class Widget; template<typename T> class Widget;
然而,C++ 并不总是把class 和 typename 视为等价。有时候你一定得使用typename。考虑下面代码:
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参数,称之为从属名称。
- 如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称。
C::const_iterator就是这样一个名称。实际上它还是个嵌套从属类型名称,也就是个嵌套从属名称并且指涉某类型。int是一个并不倚赖任何template参数的名称。这样的名称是所谓非从属名称。
上面代码的问题在于,我们认为的“C::const_iterator”是个类型,但当编译器开始解析tempalte print2nd时,尚未确知C是什么东西。C++有个规则:如果解析器在template中遭遇个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。所以缺省情况下嵌套从属名称不是类型。这也就是上面代码中“无效代码”的缘由。如果要正确使用,我们必须告诉C++说C::const_iterator是个类型。只要紧临它之前放置关键字typename即可:
template<typename C> void print2nd(const C& container) { if (container.size() >= 2) { typename C::const_iterator iter(container.begin()); // 无效代码 ... } }
规则很简单:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字typename。typename 只被用来验明嵌套从属类型名称;其他名称不该有它的存在。如下:
template<typename C> //允许使用typename void func(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 classes list 中不允许“typename” { public: explicit Derived(int x) : Base<T>::Nested(x) // member initialization list 中不允许“typename” { typename Base<T>::Nested temp; // 允许“typename” ... } .... };
有时,嵌套从属类型名称过长,我们可以考虑建立一个typedef。如下:
template<typename IterT> void workWithIterator(IterT iter) { typedef typename std::iterator_traits<IterT>::value_type value_type; value_type temp(*iter); ... }
请记住:
- 声明template参数时,前缀关键字class 和 typename 可互换。
- 请使用关键字 typename 标识嵌套从属类型名称;但不得在base classes 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); ... }; ... class MsgInfo { ... }; template<typename Company> class MsgSender { public: ... void sendClear(const MsgInfo& info) { std::string msg; // here ,根据info产生信息 Company c; c.sendCleartext(msg); } void sendSecret(const MsgInfo& info) { ... } // 调用sendEncrypted }; // 假设,现在我们想在每次发生信息时志记(log) template<typename Company> class LoggingMsgSender : public MsgSender<Company> { public: ... // 这里sendClearMsg名称不同于其父类的sendClear // 这是一个好设计,因为它避免遮掩“继承而得的名称”,条款33 // 也避免重新定义一个继承而得的non-virtual函数,条款36 void sendClearMsg(const MsgInfo& info) { // 将“传送前”的信息写至log sendClear(info); // 将“传送后”的信息写至log } ... };
上面代码的问题在于,当编译器遭遇class template LoggingMsgSender定义式时,并不知道它继承什么样的class。当然它继承的是MsgSender<Company>,但其中Company是个template参数,不到后来(当LoggingMsgSender被具现化)无法确切知道它是什么。而如果不知道Company是什么,就无法知道class MsgSender<Company>看起来像什么——更明确地说是没办法知道它是否有个sendClear函数。
// CompanyZ 坚持只能使用加密通讯 class CompanyZ { public: ... // 不提供sendCleartext函数 void sendEncrypted(const std::string& msg); ... }; // 一般性的MsgSender template 对 CompanyZ并不合适 // 因为MsgSender提供了一个sendClear函数,而这对于CompanyZ并不合理 // 可以针对CompanyZ产生一个MsgSender特化版: template<> // 一个全特化,template MsgSender针对类型CompanyZ特化了,且特化是全面性的,也就是说类型一旦被定义为CompanyZ,再也没有其他template参数可供变化。 class MsgSender <CompanyZ> { public: ... void sendSecret(const MsgInfo& info) { ... } // 调用sendEncrypted ... };
对于LoggingMsgSender,当base class 被指定为MsgSender <CompanyZ>时,调用sendClearMsg仍不合法,因为CompanyZ并未提供sendClear函数!这就是为什么C++拒绝这个调用的原因:它知道base class templates有可能被特化,而那个特化版本可能不提供和一般性template相同的接口。因此它往往拒绝在templatized base classes(模板化基类)内寻找继承而来的名称。就某种意义而言,当我们从Object Oriented C++ 跨进 Template C++(条款01),继承就不像以前那般畅行无阻了。
有三个办法令C++“不进入templatized base classes观察”的行为失效。
(1)在base class 函数调用动作之前加上“this->":
template<typename Company> class LoggingMsgSender : public MsgSender<Company> { public: ... void sendClearMsg(const MsgInfo& info) { // 将“传送前”的信息写至log this->sendClear(info); //成立,假设sendClear将被继承 // 将“传送后”的信息写至log } ... };
(2)使用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); // 将“传送后”的信息写至log } ... };
(3)明白指出被调用的函数位于base class 内:
template<typename Company> class LoggingMsgSender : public MsgSender<Company> { public: ... void sendClearMsg(const MsgInfo& info) { // 将“传送前”的信息写至log // OK, 假设sendClear将被继承下来 // 但有一个问题,如果被调用的是virtual函数, // 下面语句的明确资格修饰会关闭“virtual绑定行为” MsgSender<Company>::sendClear(info); // 将“传送后”的信息写至log } ... }
但如果被调用的是virtual函数,上述的明确资格修饰会关闭virtual绑定行为。
从名称可视化的角度出发,对编译器承若“base class template”的任何特化版本都支持一般(泛化)版本所提供的接口,
请记住:
- 可在derived class templates 内通过 “this->”指涉base class templates内的成员名称,或藉由一个明白写出的“base class 资格修饰符”完成。
条款44 : 将与参数无关的代码抽离templates
本条款无非采用“共性与变性分析”(找出共同部分和变化部分,分别分析处理)。考虑如下代码:
template<typename T, // 类型参数 std::size_t n> // 非类型参数 class SquareMatrix { public: .... void invert(); // 支持矩阵逆置 }; // 如下调用会具现化两份invert。 SquareMatrix<double, 5> sm1; ... sm1.invert(); // 调用SquareMatrix<double, 5>::invert SquareMatrix<double, 10> sm2; ... sm2.invert(); // 调用SquareMatrix<double, 10>::invert
(1)下面对SquareMatrix作第一次修改:
// SquareMatrixBase只对“矩阵元素对象的类型”参数化 // 因此对于某给定之元素对象类型,所有矩阵共享同一个(也是 // 唯一一个)SquareMatrixBase class。它们也将因此共享 // 这唯一一个class内的invert template<typename T> // 只对“矩阵元素对象的类型”参数化 class SquareMatrixBase { protected: .... void invert(std::size_t matrixSize); }; template<typename T, std::size_t n> class SquareMatrixBase : private SquareMatrixBase<T> // private, 条款39 { private: using SquareMatrixBase<T>::invert; // 避免遮掩base版的invert,条款33 public: .... void invert() { this->invert(n); } // "this->" 记号,条款43 };
(2)上面代码仍存在一个问题:SquareMatrixBase::invert不知道该操作什么数据,想必只有derived class知道, 所以derived class必须想办法向base class传递这个信息。一个可能的做法是为SquareMatrixBase::invert添加另一个参数,也许是个指针,指向一块用来放置矩阵数据的内存起始点。但我们可能需要对其他每一个需要此参数的SquareMatrixBase内的函数一次次传递这个信息。改进办法是令SquareMatrixBase贮存一个指针,指向矩阵数值所在的内存。如下:
// “持有同型元素”(不论矩阵大小)之所有矩阵共享 template<typename T> // 只对“矩阵元素对象的类型”参数化 class SquareMatrixBase { protected: SquareMatrixBase(std::size_t n, T*pMem) // 存储矩阵大小和一个指针,指向矩阵数值 : size(n), pData(pMem) { } ... private: std::size_t size; // 矩阵大小 T*pData; // 指针,指向矩阵数值 }; // SquareMatrix对象有着不同的类型 // method1:某些实现版本也许会决定将矩阵数据存储在SquareMatrix对象内部 // 这可能导致对象自身非常大 template<typename T, std::size_t n> class SquareMatrix : private SquareMatrixBase<T> { public: SquareMatrix() : SquareMatrixBase<T>(n, data) { } ... private: T data[n*n]; }; // method2:把每一个矩阵的数据放进heap(通过new来分配内存) // SquareMatrix对象有着不同的类型 template<typename T, std::size_t n> class SquareMatrix : private SquareMatrixBase<T> { public: SquareMatrix() : SquareMatrixBase<T>(n, 0) // 将base class的数据指针设为null pData(new T[n*n]) // 为矩阵内容分配内存,将指向该内存的指针存储起来 { this->setDataPtr(pData.get()); } // 将它的一个副本交给base class ... private: boost::scoped_array<T> pData; // 条款13 };
经过上述修改,SquareMatrix成员函数可以单纯地以inline方式调用base class版本,(1)后者由“持有同型元素”(不论矩阵大小)之所有矩阵共享。与此同时,不同大小的(2)SquareMatrix对象有着不同的类型,所以即使(例如SquareMatrix<double, 5>和SquareMatrix<double, 10>)对象使用相同的SquareMatrixBase<double>成员函数,我们也没机会传递一个SquareMatrix<double, 5>对象到一个期望获得SquareMatrix<double, 10>的函数去。
硬是绑定矩阵尺寸(原始版本)的那个invert版本,有可能生成比共享版本更佳的代码;另一方面,不同大小的矩阵只拥有单一版本的invert(修改后的版本),可减少执行文件大小,也就因此降低程序的working set大小,并强化指令高速缓存区内的引用集中化。对象大小有时也是一个需要考虑的效能评比主题。
请记住:
- Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
- 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class 成员变量替换template参数。
- 因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码。
条款45 : 运用成员函数模板接受所有兼容类型
所谓“智能指针”是“行为像指针”的对象,并提供指针没有的机能。STL容器的迭代器几乎总是智能指针。真实指针做得很好的一件事是,支持隐式转换。Derived class 指针可以隐式转换为base class 指针,"指向non-const对象”的指针可以转换为“指向const对象”......等等。但如果是用户自定的智能指针,则稍微有点麻烦:
// 原始指针 class Top { ... }; class Middle : public Top { ... }; class Bottom : public Middle { ... }; Top* pt1 = new Middle; Top* pt2 = new Bottom; const Top* pct2 = pt1; // 智能指针 template<typename T> class SmartPtr { public: explicit SmartPtr(T* realPtr); ... }; SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle); SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom); SmartPtr<const Top> pct2 = pt1;
templates和泛型编程
在上面智能指针实例中,每一个语句创建了一个新式智能指针对象,所以现在我们应该关注如何编写智能指针的构造函数,使其行为能够满足我们的转型需要。一个很具观察结果是:我们永远无法写出我们需要的所有构造函数。因此,似乎我们需要的不是为SmartPtr写一个构造函数,而是为它写一个构造模板。这样的模板是所谓的member function templates(常简称为 member templates),其作用是为class生成函数:
template<typename T> class SmartPtr { public: template<typename U> // member template SmartPtr(const SmartPtr<U>& other); // 为了生成copy构造函数 ... };
以上代码的意思是,对任何类型T和任何类型U,这里可以根据SmartPtr<U>生成一个SmartPtr<T>——因为SmartPtr<T>有个构造函数接受一个SmartPtr<U>参数。这一类构造函数根据对象u创建对象t,而u和t的类型是同一个template的不同具现体,有时我们称之为泛化copy构造函数。上面泛化copy构造函数并未被声明为explicit,因为我们需要此泛化构造函数支持隐式转换。
然而,此泛化构造函数提供的东西比我们需要的更多。我们需要根据一个SmartPtr<Bottom>创建一个SmartPtr<Top>,却不希望根据一个SmartPtr<Top>创建一个SmartPtr<Bottom>;我们也不希望根据一个SmartPtr<double>创建一个SmartPtr<int>。因此,我们必须从某方面对这一member template所创建的成员函数群进行拣选或筛除。假设SmartPtr遵循auto_ptr 和 tr1::shared_ptr所提供的榜样,也提供一个get成员函数,返回智能指针对象(条款15)所持有的那个原始指针的副本,那么:
template<typename T> class SmartPtr { public: template<typename U> // member template SmartPtr(const SmartPtr<U>& other) : heldPtr(other.get()) { ... } T* get() const { return heldPtr; } ... private: T* heldPtr; // 持有内置(原始)指针 };
我们使用成员初值列来初始化SmartPtr<T>之内类型为T* 的成员变量,并以类型为U*的指针作为初值。这个行为只有当“存在某个隐式转换可将一个U*指针转为一个T*指针”时才能通过编译。
member function templates(成员函数模板)的效用不限于构造函数,他们常扮演的另一个角色是支持赋值操作。
template<class T> class shared_ptr { public: template<Y> explicit shared_ptr(Y* p); // 构造函数 template<Y> shared_ptr(shared_ptr<Y> const& r); // copy构造函数 template<Y> explicit shared_ptr(weak_ptr<Y> const& r); template<Y> explicit shared_ptr(auto_ptr<Y> & r); // 不带const,auto_ptr会改变传入的对象 template<Y> shared_ptr& operator=(shared_ptr<Y> const& r); // copy赋值操作符 template<Y> shared_ptr& operator=(auto_ptr<Y> & r); ... };
上述所有构造函数都是explicit,唯有“泛化copy构造函数”除外。那意味从某个shared_ptr类型隐式转换至另一个shared_ptr类型是被允许的(调用到copy构造函数),但从某个内置指针或从其他智能指针类型进行隐式转换则不被认可。另一点,传递给tr1::shared_ptr构造函数和assignment操作符的auto_ptr并未被声明为const,这是因为条款13说过,当你复制一个auto_ptr,它们其实被改动了。
member function templates(成员函数模板)并不改变语言规则,而语言规则说,如果程序需要一个copy构造函数,你却没有声明它,编译器会为你暗自生成一个。在class内声明泛化copy构造函数(是个member template)并不会阻止编译器生成它们自己的copy构造函数(一个non-template),所以如果你想要控制copy构造的方方面面,你必须同时声明泛化copy构造函数和“正常的”copy构造函数。相同规则也适用于赋值操作。如下:
template<class T> class shared_ptr { public: shared_ptr(shared_ptr const& r); // non-template copy构造函数 template<Y> shared_ptr(shared_ptr<Y> const& r); // copy构造函数 shared_ptr& operator=(shared_ptr const& r); // non-template copy赋值操作符 template<Y> shared_ptr& operator=(shared_ptr<Y> const& r); // copy赋值操作符 ... };
请记住:
- 请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。
- 如果你声明member templates用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。(member templates并不改变语言规则)。
条款46 : 需要类型转换时请为模板定义非成员函数
条款24讨论过为什么唯有non-member函数才有能力“在所有实参身上实施隐式类型转换”,本条款在此基础上将条款24例子中的Rational和operator*模板化了:
template<typename T> class Rational { public: Rational(const T& numerator = 0, // 条款20告诉你为什么参数以passed by reference方式传递 const T& denominator = 1); const T numerator() const; // 条款28告诉你为什么返回值以passed by value方式传递 const T numerator() const; // 条款03告诉你为什么它们是const ... }; // 非类成员,亦非友元,而仅仅只是一个function templates template<typename T> const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) { .... } // 我们希望一下调用顺利通过编译,毕竟它正是条款24所列同一份代码,不同的仅是成了templates: Rational<int> oneHalf(1, 2); // ok Rational<int> result = oneHalf * 2; //错误,无法通过编译
上述失败给我们的启示是,模板的Rational内的某些东西似乎和其non-template版本不同。事实的确如此。在条款24内,编译器知道我们尝试调用声明函数,但这里编译器不知道我们想要调用哪个函数。取而代之的是,它们试图想出什么函数被名为operator*的template具现化(产生)出来。它们知道它们应该可以具现化某个“名为operator*并接受两个Rational<T>参数”的函数,但为了完成这一具现化行动,必须先算出T是什么。问题是它们没有这个能耐。
以oneHalf进行推导,过程并不困难。operator*的第一参数被声明为Rational<int>,而传递给operator*的第一参数的类型为Rational<int>,所以T一定是int。operator*的第二参数被声明为Rational<int>,但传递给operator*的第二参数的类型为int。编译器如何根据这个推算出T?你或许会期盼编译器使用Rational<int>的non-explicit构造函数将int转换为Rational<int>,进而将T推导为int,但它们不那么做,因为在template实参推导过程中从不将隐式类型转换函数纳入考虑(需要类型完全匹配)。绝不!这样的转换在函数调用过程中的确被使用,但在能够调用一个函数之前,首先必须知道那个函数存在。而为了知道它,必须先为相关的function template推导出参数类型(然后才可将适当的函数具现化出来以供调用)。然而,template实参推导过程中并不考虑采纳“通过构造函数而发生的”隐式类型转换。
只要利用一个事实,我们就可以缓和编译器在template实参推导方面受到的挑战:template class 内的friend 声明式可以指涉某个特定函数。那意味class Rational<T>可以声明operator*是它的一个friend函数。Class templates并不倚赖template实参推导(后者只施行于function templates身上),所以编译器总是能够在class Rational<T>具现化时得知T。
template<typename T> class Rational { public: // 定义成non-member函数在于支持“在所有实参身上实施隐式类型转换”,operator*函数定义在Rational类里面,是为了此函数被自动具现化,原因如下: // 当Rational对象被声明的时候,class Rational<int>也就被具现化出来了,而作为过程的一部分,friend函数operator*也就被自动具现化. // 现在可以通过编译,却仍然还不能通过链接,因为只提供了函数声明,而没有提供函数定义,试图使用class外的operator*函数定义是不行的:如果我们自己声明了一个函数,就有责任定义那个函数 friend // 声明operator*函数 const Rational operator*(const Rational& lhs, const Rational& rhs); // 上面语句等价“friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);” }; template<typename T> // 定义operator*函数 const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) { .... }
现在对operator*的混合式调用可以通过编译了,因为当对象oneHalf被声明为一个Rational<int>,class Rational<int>于是被具现化出来,而作为过程的一部分,friend函数operator*(接受Rational<int>参数)也就被自动声明出来(成为一个特定函数(函数已经是存在状态),而不再是一个函数模板,也即不再需要进行“模板(template)实参推导”,直接进入函数调用时的实参推导,而此时可以使用隐式转换函数)。后者身为一个函数而非函数模板,因此编译器可在调用它时使用隐式转换函数(例如Rational 的non-explicit构造函数)。这便是混合式调用之所以成功的原因。但此段代码虽可以通过编译,却无法连接。这里暂时插播一下“Rational内声明operator*的语法”。
在一个class template内,template 名称可被用来作为“template 和其参数”的简略表示方式,所以在Rational<T>内我们可以只写Rational而不必写Rational<int>。
现在回头考虑上面无法连接的问题:混合式代码通过了编译,因为编译器知道我们要调用哪个函数,但那个函数只被声明于Rational内,并没有被定义出来。我们意图令此class外部的operator* template 提供定义式,但是行不通——如果我们自己声明了一个函数,就有责任定义那个函数。(friend函数可以在类外进行定义,这里无法连接,是因为此friend函数为template吗?)既然我们没有提供定义式,连接器当然找不到它!
(1)一个简单的方法就是将operator*函数本体合并至其声明式内:
template<typename T> class Rational { public: friend // 声明operator*函数 const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); } };
我们虽然使用friend,却与friend的传统用途“访问class 的non-public成分”毫不相干。为了让类型转换可能发生于所有实参身上,我们需要一个non-member函数;为了令这个函数被自动具现化,我们需要将它声明在class内部;而在class内部声明non-member函数的唯一办法就是:令它成为一个friend。
(2)条款30所说,定义于class 内的函数都暗自成为inline,包括像operator*这样的friend函数。你可以将这样的inline声明所带来的冲击最小化,做法是令operator*不做任何事情,只调用一个定义于class外部的辅助函数。“令friend函数调用辅助函数”对于复杂的函数而言是值得的。
template<typename T> class Rational; // 前向声明Rational template template<typename T> const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs); template<typename T> class Rational { public: ..... friend // 声明operator*函数 const Rational operator*(const Rational& lhs, const Rational& rhs) // Rational已具现化 { return doMultiply(lhs, rhs); } ... }; // 许多编译器实质上会强迫你把所有template定义式放进头文件内 template<typename T> // 若有必要,在头文件内定义helper template const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs); { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); }
作为一个template,doMultiply当然不支持混合式乘法,但它其实也不需要。它只被operator*调用,而operator*支持了混合式操作(通过隐式转换函数完成)。
请记住:
- 当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template 内部的friend函数”。
条款47 : 请使用traits classes 表现类型信息
STL主要由“用以表现容器、迭代器和算法”的templates构成,但也覆盖若干工具性templates,其中一个名为advance,用来将某个迭代器移动某个给定距离:
template<typename IterT, typename DistT> void advance(IterT& iter, DistT d); // d < 0 则向后移动
STL有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 { };
既然我们已经知道STL迭代器有着不同的能力,那么我们会希望运用其优势:
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迭代器。这就是traits做的事:它们允许你在编译期间取得某些类型信息。
“traits必须能够施行于内置类型”意味“类型内的嵌套信息”这种东西出局了,因为我们无法将信息嵌套于原始指针内。因此类型的traits信息必须位于类型自身之外。标准技术是把它放进一个template及其一个或多个特化版本中。这样的templates在标准程序库中有若干个,其中针对迭代器者被命名为iterator_traits:
template<typename IterT>
struct iterator_traits;
iterator_traits的运作方式是,针对每一个类型IterT,在struct iterator_traits<IterT>内一定声明某个typedef名为iterator_category。这个typedef用来确认IterT的迭代器分类。
iterator_traits以两个部分实现上述所言。首先它要求每一个“用户自定义的迭代器类型”必须嵌套一个typedef,名为iterator_category,用来确认适当的卷标结构(tag struct)。如deque的迭代器可随机访问,所以一个针对deque迭代器而设计的class看起来会是这样:
template< ... > class deque { public: class iterator { public: typedef random_access_iterator_tag iterator_category; ... }; ... }; // 至于iterator_traits,只是鹦鹉学舌般的响应iterator class 的嵌套式typedef: template<typename IterT> struct iterator_traits { typedef typename IterT::iterator_category iterator_category; .... };
这对用户自定义类型行得通,但对指针(也是一种迭代器)行不通,因为指针不可能嵌套typedef(所以不存在“IterT::iterator_category”)。为了支持指针迭代器,iterator_traits特别针对指针类型提供一个偏特化版本。由于指针的行径与random access迭代器类似,所以iterator_traits为指针指定的迭代器类型是:
template<typename IterT> struct iterator_traits<IterT*> // template 偏特化,针对内置指针 { typedef random_access_iterator_tag iterator_category; .... };
总结一下如何设计并实现一个traits class:
- 确认若干你希望将来可取得的类型信息。例如对迭代器而言,我们希望将来可取得其分类(category)。
- 为该信息选择一个名称(例如 iterator_category)。
- 提供一个template 和一组特化版本(针对指针),内含你希望支持的类型相关信息。
现在,我们有了iterator_traits(实际上是std::iterator_traits,因为它是C++标准程序库的一部分),我们可以对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)) ... }
首先,上面代码会导致编译问题(条款48),其次,IterT类型在编译期间获知,所以std::iterator_traits<IterT>::iterator_category也可在编译期间确定。但if语句却是在运行期才会核定。为什么将可在编译期完成的事延到运行期才做呢?这不仅浪费时间,也造成可执行文件膨胀。
我们真正想要的是一个条件式判断“编译期核定成功”之类型。恰巧C++有一个取得这种行为的办法:重载(利用编译期完成的“重载解析机制”)。。当你重载某个函数f,你必须详细叙述各个重载件的参数类型,而编译器便会在编译期间匹配最适当的那个重载函数。如下:
template<typename IterT, typename DistT> void doAdvance(IterT& iter, DistT d // random access 迭代器 std::random_access_iterator_tag) { iter += d; } template<typename IterT, typename DistT> void doAdvance(IterT& iter, DistT d std::bidirectional_access_iterator_tag) { ..... } // forward_iterator_tag public 继承自input_iterator_tag template<typename IterT, typename DistT> void doAdvance(IterT& iter, DistT d std::input_iterator_tag) { .... } // 有了这些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:
- 建立一组重载函数或函数模板(doAdvance),彼此间的差异只在于各自的traits参数。令每个函数实现码与其接受之traits信息相应和。
- 建立一个控制函数或函数模板(advance),它调用上述那些doAdvance函数并传递traits class 所提供的信息。
Traits 广泛用于标准程序库。其中当然有上述讨论的iterator_traits,除了供应iterator_category还供应另四份迭代器相关信息(其中最有用的是value_type,条款42)。此外还有char_traits用来保持字符类型的相关信息,以及numericlimits用来保存数值类型的相关信息,例如某数值类型可表现之最小值和最大值等等;命名为numeric_limits没有遵守traits class 常以“traits”结束的风格。
TR1(条款54)导入许多新的traits classes用以提供类型信息,包括is_fundamental<T>(判断T是否为内置类型),is_array<T>(判断T是否为数组类型),以及is_base_of<T1, T2>(T1和T2相同,抑或T1是T2的base class)。总计TR1一共为标准C++添加了50个以上的traits classes。
请记住:
- Traits classes使得“类型相关信息”在编译期可用。它们以templates(针对用户自定义类型)和“templates特化”(针对指针)完成实现。
- 整合重载技术后,traits classes 有可能在编译期对类型执行if....else测试。(利用编译期完成的“重载解析机制”)。
条款48 : 认识template元编程
Template metaprogramming(TMP,模板元编程)是编写template-based C++程序并执行于编译期的过程。所谓Template metaprogram(模板元程序)是以C++写出、执行于C++编译期内的程序。一旦TMP程序结束执行,其输出,也就是templates具现出来的若干C++源码,便会一如往常地被编译。
C++并非是为Template metaprogramming而设计,但自从TMP于1990s初期被发现以后,由于日渐被证明十分有用,其延伸部分很可能加入语言和标准程序库内,使TMP更容易进行。
TMP有两个伟大的效力。第一,它让某些事情更容易。如果没有它,那些事情将是困难的,甚至不可能的。第二,由于Template metaprograms执行于C++编译期,因此可将工作从运行期转移到编译期。这导致的一个结果是,某些错误原本通常在运行期才能侦测到,现在可在编译期找出来。另一个结果是,使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。然而将工作从运行期转至编译期的另一个结果是,编译时间变长了。
条款47中有如下伪码:
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; } } }
我们可以使用typeid让其中的伪码成真,取得C++对此问题的一个“正常”解决方案——所有工作都在运行期进行:
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)) ... }
条款47指出,这个typeid-based解法的效率比traits解法低,因为在此方案中,(1)类型测试发生于运行期而非编译期,(2)“运行期类型测试”代码会出现在(或说被连接于)可执行文件中。实际上这个例子正可彰显TMP如何能够比“正常的”C++程序更高效,因为traits解法就是TMP。别忘了,traits引发“编译期发生于类型身上的if...else计算”。同时,条款47曾提过advance的typeid-based实现方式可能导致编译期问题,下面就是个例子:
std::list<int>::iterator iter; ... advance(iter, 10); // 无法通过编译 // 针对上述调用,我们会得到这些: void advance(std::list<int>::iterator& iter, int d) { if (typeid(typename std::iterator_traits<std::list<int>::iterator>::iterator_category) == typeid(std::random_access_iterator_tag)) { iter += d; // 错误 } else { if (d >= 0) { while(d--) ++iter; } else { while(d++) --iter; } } }
问题出在我所强调的那一行代码使用了 += 操作符,那便是尝试在一个std::list<int>::iterator身上使用 += ,但是std::list<int>::iterator是个bidirectional迭代器,并不支持+=。只有random access 迭代器才支持 += 。此刻我们知道绝不会执行起 += 那一行,因为测试typeid的那一行总是会因为std::list<int>::iterators而失败,但编译器必须确保所有源码都有效,纵使是不会执行起来的代码!而当iter不是random access 迭代器时“iter += d”无效。与此对比的是traits-based TMP解法,其针对不同类型而进行的代码,被拆分为不同的函数,每个函数所使用的操作(操作符)都可施行于该函数所对付的类型。
TMP已被证明是个“图灵完全”机器,意思是它的威力大到足以计算任何事物。使用TMP你可以声明变量、执行循环、编写及调用函数……但这般构件相对于“正常的”C++对应物看起来很是不同,例如条款47所展示的TMP if...else条件句是藉由templates和其特化体表现出来。不过那毕竟是汇编语言层级的TMP。针对TMP而设计的程序库(例如Boost’s MPL,条款55)提供更高层级的语法。
为了再次浮光掠影地认识一下“事物在TMP中如何运作”,让我们看看循环。TMP并没有真正的循环构件,所以循环效果系藉由递归完成。TMP主要是个“函数式语言”,而递归之于这类语言就像电视之于美国通俗文化一样地无法分割。TMP的递归甚至不是正常种类,因为TMP循环并不涉及递归函数调用,而是涉及“递归模板具现化”。TMP的起手程序是在编译期计算阶乘:
template<unsigned n> struct Factorial { enum { value = n * Factorial<n-1>::value }; }; template<> // 递归终结,模板特化 struct Factorial<0> { enum { value = 1 }; }; // 你便可以这样调用 std::cout << Factorial<5>::value; // 打印120
为求领悟TMP之所以值得学习,很重要一点是先对它能够达成什么目标有一个比较好的理解:
- 确保量度单位正确。在科学和工程应用程序中,确保量度单位(例如质量、距离、时间……)正确结合是绝对必要的。如果使用TMP,就可以确保(在编译期)程序中所有量度单位的组合都正确,不论其计算多么复杂。
- 优化矩阵运算。如果使用高级、于TMP相关的template技术,即所谓expression templates,就有可能消除那些临时对象并合并循环,这一切都无需改变客户端的用法。
- 可以生产客户端定制之设计模式实现品。设计模式如Strategy(条款35),Observer,Visitor等等都可以多种方式实现出来。运用所谓policy-based design之TMP-based技术,有可能产生一些templatles用来表述独立的设计选项,然后可以任意结合它们,导致模式实现品带着客户定制的行为。这项技术已被用来让若干templates实现出智能指针的行为政策,用以在编译期间生成数以百计不同的智能指针类型。这项技术已经超越编程工艺领域如设计模式和智能指针,更广义地成为generative programming(殖生式编程)的一个基础。
请记住:
- Template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
- TMP 可被用来生成“基于政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。