Effective C++ —— 模板与泛型编程(七)
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() > && 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() > && w != someNastyWidget)
{
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
(1)w 必须支持哪一种接口,系由template中执行于w身上的操作来决定。本例看来w的类型T好像必须支持size,normalize和swap成员函数,copying函数(用来建立temp),不等比较(用来比较someNasty-Widget)等等。这一组表达式(对此template而言必须有效编译)便是T必须支持的一组隐式接口。
(2)凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化,使这些调用得以成功。这样的具现行为发生在编译期。“以不同的template参数具现化function templates(函数模板)”会导致调用不同的函数,这个便是所谓的编译期多态。(“哪一个函数应该被调用”——发生在编译期; “哪一个virtual函数该被绑定”——发生在运行期)。
通常,显式接口由函数签名式(也就是函数名称、参数类型、返回类型)构成。
隐式接口就完全不同了。它并不基于函数签名式,而是由有效表达式组成。
故而:
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() >= )
{
C::const_iterator iter(container.begin()); // 无效代码
++iter;
int value = *iter;
std::cout << value;
}
}
代码中有两个local变量。iter的类型是C::const_iterator,实际是什么必须取决于template参数C。(1)template内出现的名称如果相依于某个template参数,称之为从属名称。(2)如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称。C::const_iterator就是这样一个名称。实际上它还是个(3)嵌套从属类型名称,也就是个嵌套从属名称并且指涉某类型。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() >= )
{
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);
...
}
故而:
1. 声明template参数时,前缀关键字class 和 typename 可互换。
2. 请使用关键字 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<> // 一个全特化
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声明式,条款33.
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
}
...
};
故而:
可在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, > sm1;
...
sm1.invert(); // 调用SquareMatrix<double, 5>::invert
SquareMatrix<double, > 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, ) // 将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大小,并强化指令高速缓存区内的引用集中化。对象大小有时也是一个需要考虑的效能评比主题。
故而:
1. Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
2. 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class 成员变量替换template参数。
3. 因类型参数(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;
上面智能指针转换问题在于,对于同一个template的不同具现体之间并不存在什么与生俱来的固有关系。所以编译器视SmartPtr<Middle>和SmartPtr<Top>为完全不同的classes。
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,因为我们需要此泛化构造函数支持隐式转换(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构造函,没有被声明为explicit,支持隐式转换
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赋值操作符
...
};
故而:
1. 请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。
2. 如果你声明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 = , // 条款20告诉你为什么参数以passed by reference方式传递
const T& denominator = );
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(, ); // ok
Rational<int> result = oneHalf * ; //错误,无法通过编译
上述失败给我们的启示是,模板的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 >= ) { 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:
(1)确认若干你希望将来可取得的类型信息。例如对迭代器而言,我们希望将来可取得其分类(category)。
(2)为该信息选择一个名称(例如 iterator_category)。
(3)提供一个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:
(1)建立一组重载函数或函数模板(doAdvance),彼此间的差异只在于各自的traits参数。令每个函数实现码与其接受之traits信息相应和。
(2)建立一个控制函数或函数模板(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。
故而:
1. Traits classes使得“类型相关信息”在编译期可用。它们以templates(针对用户自定义类型)和“templates特化”(针对指针)完成实现。
2. 整合重载技术后,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 >= ) { 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, ); // 无法通过编译
// 针对上述调用,我们会得到这些:
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 >= ) { 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->::value };
};
template<> // 递归终结,模板特化
struct Factorial<>
{
enum { value = };
}; // 你便可以这样调用
std::cout << Factorial<>::value; // 打印120
为求领悟TMP之所以值得学习,很重要一点是先对它能够达成什么目标有一个比较好的理解:
(1)确保量度单位正确。在科学和工程应用程序中,确保量度单位(例如质量、距离、时间……)正确结合是绝对必要的。如果使用TMP,就可以确保(在编译期)程序中所有量度单位的组合都正确,不论其计算多么复杂。
(2)优化矩阵运算。如果使用高级、于TMP相关的template技术,即所谓expression templates,就有可能消除那些临时对象并合并循环,这一切都无需改变客户端的用法。
(3)可以生产客户端定制之设计模式实现品。设计模式如Strategy(条款35),Observer,Visitor等等都可以多种方式实现出来。运用所谓policy-based design之TMP-based技术,有可能产生一些templatles用来表述独立的设计选项,然后可以任意结合它们,导致模式实现品带着客户定制的行为。这项技术已被用来让若干templates实现出智能指针的行为政策,用以在编译期间生成数以百计不同的智能指针类型。这项技术已经超越编程工艺领域如设计模式和智能指针,更广义地成为generative programming(殖生式编程)的一个基础。
故而:
1. Template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率。
2. TMP 可被用来生成“基于政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。
Effective C++ —— 模板与泛型编程(七)的更多相关文章
- Effective C++ ——模板和泛型编程
条款41:了解隐式接口和编译器多态 以public继承的类,
- 【effective c++】模板与泛型编程
模板元编程:在c++编译器内执行并于编译完成时停止执行 1.了解隐式接口和编译期多态 面向对象编程总是以显式接口(由函数名称.参数类型和返回类型构成)和运行期多态(虚函数)解决问题 模板及泛型编程:对 ...
- C++ 模板与泛型编程
<C++ Primer 4th>读书笔记 所谓泛型编程就是以独立于任何特定类型的方式编写代码.泛型编程与面向对象编程一样,都依赖于某种形式的多态性. 面向对象编程中的多态性在运行时应用于存 ...
- C++ Primer 学习笔记_76_模板与泛型编程 --模板定义[续]
模板与泛型编程 --模板定义[续] 四.模板类型形參 类型形參由keywordclass或 typename后接说明符构成.在模板形參表中,这两个keyword具有同样的含义,都指出后面所接的名字表示 ...
- C++ Primer 学习笔记_84_模板与泛型编程 --模板特化
模板与泛型编程 --模板特化 引言: 我们并不总是能够写出对全部可能被实例化的类型都最合适的模板.某些情况下,通用模板定义对于某个类型可能是全然错误的,通用模板定义或许不能编译或者做错误的事情;另外一 ...
- C++ Primer 学习笔记_77_模板与泛型编程 --实例化
模板与泛型编程 --实例化 引言: 模板是一个蓝图,它本身不是类或函数.编译器使用模板产生指定的类或函数的特定版本号.产生模板的特定类型实例的过程称为实例化. 模板在使用时将进行实例化,类模板在引用实 ...
- C++ Primer 学习笔记_85_模板与泛型编程 --模板特化[续]
模板与泛型编程 --模板特化[续] 三.特化成员而不特化类 除了特化整个模板之外,还能够仅仅特化push和pop成员.我们将特化push成员以复制字符数组,而且特化pop成员以释放该副本使用的内存: ...
- C++ Primer 学习笔记_75_模板与泛型编程 --模板定义
模板与泛型编程 --模板定义 引言: 所谓泛型程序就是以独立于不论什么特定类型的方式编写代码.使用泛型程序时,我们须要提供详细程序实例所操作的类型或值. 模板是泛型编程的基础.使用模板时能够无须了解模 ...
- C++ Primer 学习笔记_76_模板和泛型编程 --模板定义[继续]
模板和泛型编程 --模板定义[续] 四.模板类型形參 类型形參由keywordclass或 typename后接说明符构成.在模板形參表中,这两个keyword具有同样的含义,都指出后面所接的名字表示 ...
随机推荐
- SharePoint 项目的死法(三)
拙劣的供应商(团队) 坦率来说, 说这个原因需要一点勇气, 但在我从业的经历中, 充斥这大量的这样的案例, 没有什么实施经验的团队, 对产品几乎没什么了解的供应商, 三脚猫的开发人员,之前只会做做微软 ...
- [转载]AMD 的 CommonJS wrapping
https://www.imququ.com/post/amd-simplified-commonjs-wrapping.html 它是什么? 为了复用已有的 CommonJS 模块,AMD 规定了 ...
- 2014年最佳的10款 PHP 开发框架
PHP去年发生了翻天覆地的变化.似乎每个人都有一个想法一个好的框架应该是什么样子,但话又说回来,没有多少面积制品类型的框架或框架的最终实际使用在不同的生产项目. 你知道哪个框架选择为您的生产计划吗?你 ...
- 列表函数&方法
列表(list)的基本操作,方法及属性.
- sklearn_k邻近分类_KNeighborsClassifier
# coding:utf-8 import numpy as np import matplotlib.pyplot as plt from sklearn.neighbors import KNei ...
- Python练习-无参装饰器的正确打开方式
import time def DecoUserPrint(UserFunc):#定义一个DecoUserPrint接收参数的多重方法 def DecoPrint(): StartTime = tim ...
- Linux的基础优化-2
1.启动网卡 ifup eth0 2.SSH链接 ifconfig 查看IP后SSH终端连接3.更新源 最小化安装是没有wget工具的,必须先安装再修改源 yum install wget 备份原系统 ...
- [转]QList内存释放
QList<T> 的释放分两种情况: 1.T的类型为非指针,这时候直接调用clear()方法就可以释放了,看如下测试代码 #include <QtCore/QCoreApplicat ...
- css单行文本和多行文本溢出实现省略号显示
1.单行文本溢出 文本内容 <div class="singleLine"> HelloWorldHelloWorldHelloWorldHelloWorldHello ...
- 2015 Dhaka
2015 Dhaka A - Automatic Cheater Detection solution 模拟计数. B - Counting Weekend Days solution 模拟计数. C ...