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具有同样的含义,都指出后面所接的名字表示 ...
随机推荐
- 关于安装在win10上的oracle10g 兼容性问题
首先在安装过程中会出现一次报错,在安装的时候 安装好了以后,准备敲击命令如果出现闪退,即是兼容性问题,下面继续设置兼容性问题 然后右键----属性----兼容性,勾上以兼容性运行即可
- VBS 重启 TP-Link 路由器
分享一个自己用的小工具,重启TP-Link路由器的,好像还是大学时候写的,献丑了. 其他路由器可能有些不同,但是思路都是差不多的. user = "admin" '路由器帐号 pa ...
- @Controller,@Service,@Repository,@Component详解
@Controller 用来表示一个web控制层bean,如SpringMvc中的控制器. @Service 用来表示一个业务层bean. @Repository 用来表示一个持久层bean,即数据访 ...
- elasticsearch-head插件安装说明
下载及安装readme https://github.com/mobz/elasticsearch-head 安装: npm install npm run start 访问:http://local ...
- 20155226 2016-2017-2 《Java程序设计》第5周学习总结
20155226 2016-2017-2 <Java程序设计>第5周学习总结 教材学习内容总结 语法与继承构架 我们之前接触到的C通常都是将程序流程和错误处理混在一起,在编写程序的时候必须 ...
- 外卖(food) & 洛谷4040宅男计划 三分套二分&贪心
food评测传送门 [题目描述] 叫外卖是一个技术活,宅男宅女们一直面对着一个很大的矛盾,如何以有限的金钱在宿舍宅得尽量久. 外卖店一共有 N 种食物,每种食物有固定的价钱 Pi 与保质期 Si ...
- 【多视图几何】TUM 课程 第3章 透视投影
课程的 YouTube 地址为:https://www.youtube.com/playlist?list=PLTBdjV_4f-EJn6udZ34tht9EVIW7lbeo4 .视频评论区可以找到课 ...
- 洛谷 P3307: bzoj 3202: [SDOI2013] 项链
题目传送门:洛谷P3307.这题在bzoj上是权限题. 题意简述: 这题分为两个部分: ① 有一些珠子,每个珠子可以看成一个无序三元组.三元组要满足三个数都在$1$到$m$之间,并且三个数互质,两个珠 ...
- [NOI2007]货币兑换 「CDQ分治实现斜率优化」
首先每次买卖一定是在某天 $k$ 以当时的最大收入买入,再到第 $i$ 天卖出,那么易得方程: $$f_i = \max \{\frac{A_iRate_kf_k}{A_kRate_k + B_k} ...
- mybatis比hibernate处理速度快的原因
mybatis:是面向结果集的.当要展示的页面需要几个字段时,springmvc会提供这几个字段并将其拼接成结果集,在转化为相应的对象. hibernate:是面向对象的.要展示的页面需要某些字段时, ...