(Effective C++)第七章 模板与泛型编程 (Templates and Generic Programming)

来源:互联网 发布:俄罗斯方块知乎 编辑:程序博客网 时间:2024/05/01 13:23

C++template机制自身是一部完整的图灵机(Turing-complete):它可以被用来计算任何可计算的值。于是导出了模板元编程(templatemetaprogramming),创造出“在C++编译器内执行并于编译完成时停止执行”的程序。

9.1 条款41:了解隐式接口和编译期多态 (Understand implicit interface and compile-time polymorphism)

面向对象编程世界总是以显示接口(explicit interfaces)和运行期多态(runtime polymorphism)解决问题。

class Widget{
public://函数显示接口
Widget();
virtual ~Widget();
virtual std::size_t size()const;
virtual void normalize();
void swap(Widget &other);
};
void doProccessing(Widget &w)
{
    if (w.size() > 10 && w!= someNastWidget){
     Widget temp(w);
     temp.normalize();
     temp.swap(w);
}
}
//改为模板
template <typename T>
void doProccessing(T &w)
{
    if (w.size() > 10 && w!= someNastWidget){
     T temp(w);
     temp.normalize();
     temp.swap(w);
}
}
示例9-1-1  模板
改成模板函数之前:
由于w的类型被声明为Widget,所以w必须支持Widget接口。
由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态(runtime polymorphism),也就是说将于运行期根据w的动态类型决定调用哪个函数。
改成模板函数之后:
Templates及泛型编程的世界,与面向对象有根本的不同,反倒是隐式接口(implicit interface)和编译期多态(compile-time polymorphism)移到前面了。
W必须支持哪一种接口,系由template中执行于w身上的操作来决定。本例w的类型是T必须支持size,normalize和swap成员函数,copy构造函数。这一组表达式便是T必须支持的一组隐式接口(implicit interface)。
凡是涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化(instantiated),使这些调用得以成功。“以不同的template参数具现化function templates”会导致调用不同的函数,这就是所谓的编译期多态(compile-time polymorphism)。
运行期多态和编译期多态之间的差异,类似于“哪个重载函数被调用”(发生在编译期)和“哪个virtual函数被绑定”(发生在运行期)之间的差异。
通常显示接口由函数的签名式(也就是函数名称,参数类型,返回类型)构成。
隐式接口并不基于函数签名式,而是有效表达式(valid expression)组成。如本例doProccessing的template函数。
Classes和templates都支持接口(interface)和(polymorphism)。对于classes而言接口是显式的,以函数签名为中心,多态则通过virtual函数发生在运行期。对于tempate参数而言,接口是隐式的,奠基与有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。


9.2 条款42:了解typename的双重意义 (Understand the two meaning of typename)

以下template声明式中,class和typename有什么不同?
template<class> class Widget;    //使用“class”
template<typename> class Widget;    //使用“typename”
答案:没有不同。
假设我们需要打印容器的第二个元素,如下:

template <typename C>
void print2nd(const C &container)
{
    if (container.size() > 2){
     typename C::const_iterator iter(container.begin());
     ++iter;
int valude = *iter;
std::cout<<vaule;
}
}
示例9-2-1  print2nd模板
Template内出现的名称如果相依 与某个template参数,称为从属名称(dependent names)。如果从属名称在class呈嵌套状,称为嵌套从属名称(nested dependent names)。C::const_iterator就是这样一个名称。实际上,它还是一个嵌套从属类型名称(nested dependent type name),也就是个嵌套从属名称且指涉某类型。
Local变量value是一个并不依赖任何template参数的名称,称为非从属名称(non-dependent names)。
嵌套从属名称可鞥导致解析困难,编译器便假设这名称不是一个类型,除非你告诉它是。所以缺省情况下,嵌套从属名称不是类型。
C::const_iterator被编译器假设为非类型,所以前面需要加关键字typename。

template <typename C>   //允许使用typename或class
void f(const C &container,  //不允许使用typename
typename C::iterator iter)  //一定要使用typename
{
}

template <typename T>   //允许使用typename或class
class Derived:public Base<T>::Nested{  //base class list中不允许typename
public:
explicit Derived(int)
:Base<T>::Nested(x)     //mem.init.list不允许使用typename
{
typename Base<T>::Nested temp; //一定要使用typename
}
}
//假设写一个function template,它接收一个迭代器,而我们打算为该迭代器指涉
//的对象做一份local复件temp,如下
template <typename IterT>   //允许使用typename或class
void workWithInterator(IterT iter)  
{
    typename std::iterator_traits<IterT>::vaule_type temp(*iter);
}
示例9-2-2  typename用法
使用typename关键字标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初始列)内以它作为base class修饰符。

9.3 条款43:学习处理模板化基类内的名称 (Know how to access names in templatized base class)

假设写一个程序,它能够传达信息到诺干不同的公司去。信息可能是加密的译文,可能是未经加工的文字。如果编译期间,有足够信息来决定哪一个信息传至哪家公司,就可以采用基于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);

};

class CompanyZ
{
public:
…        //这个class不提供sendCleartext
void sendEncrypted (const std::string &msg);
};
class MsgInfo {…};

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
{…}
};
//后期需要记录日志
template <typename Company>
class LoggingMsgSender::public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
    //将传送前的信息写至log
sendClear(info);
//将传送后的信息写至log
}
}
};
示例9-3-1  template解法
本例中,LoggingMsgSend调用base class函数sendClear将无法通过编译。当编译器遇到class template LoggingMsgSender定义式,并不知道它继承什么样的class。虽然它继承的是MsgSender<Company>,但是其中的Company并不知道是什么,就无法调用sendClear函数。一般性的MsgSender template对于CompanyZ并不适合,因为CompanyZ没有sendCleartext函数。与纠正这个问题,我们针对CompanyZ产生一个MsgSender特化版本。
template <> //一个全特化的MsgSender,
class MsgSender<CompanyZ>{ //他和一般的template的差别在于它删除了sendClear
public:

void sendSecret(const MsgInfo& info);
}
示例9-3-2  特化版
注意class定义式最前头的“template <>”语法是个特化版本的MsgSender template,在template实参是CompanyZ时被使用。
我们再来看LoggingMsgSender:: sendClearMsg函数,如果Company==CompanyZ,sendclear不存在,编译器往往拒绝在templatized base classes(模板化基类,本例的MsgSender<Company>)内寻找继承而来的名称。为了解决该问题,有三个办法:

//第一,在base class函数调用动作之前加上this->
template <typename Company>
class LoggingMsgSender::public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
    //将传送前的信息写至log
this->sendClear(info);
//将传送后的信息写至log
}
}
};
//第二,使用using声明书
template <typename Company>
class LoggingMsgSender::public MsgSender<Company>
{
public:
using MsgSender<Company>::sendClear;
void sendClearMsg(const MsgInfo& info)
{
    //将传送前的信息写至log
sendClear(info);
//将传送后的信息写至log
}
}
};
//第三,明白指出被调用的函数位于base class内,
//但是,不提倡,因为它关闭virtual绑定行为
template <typename Company>
class LoggingMsgSender::public MsgSender<Company>
{
public:
void sendClearMsg(const MsgInfo& info)
{
    //将传送前的信息写至log
MsgSender<Company>::sendClear(info);
//将传送后的信息写至log
}
}
};

示例9-3-3  三个方法

9.4 条款44:将与参数无关的代码抽离templates (Factor parameter-independent code out of templates)

在non-template代码中,重复十分明确:你可以“看”到两个函数或两个classes之间有所重复。然而在template代码中重复是隐晦的:毕竟只存在一份template源码,所以你必须训练自己去感受当template被具现化多次时可能发生的重复。
假设你想为固定尺寸的正方矩阵编写一个template,该矩阵的性质之一是支持逆矩阵运算(matrix inversion)。

template <typename T,std::size_t n> //template支持n*n矩阵,
class SquareMatrix{
public:

void invert(); //求逆矩阵运算
};
SquareMatrix<double, 5> sm1;
sm1.invert();  //调用SquareMatrix<double, 5>::invert
SquareMatrix<double, 10> sm2;
sm2.invert();  //调用SquareMatrix<double, 10>::invert
示例9-4-1  正方矩阵求逆运算函数的与尺寸无关版本
本例会具现化两份invert。这些函数并非完全相同,因为其中一个操作是5*5矩阵而另一个操作的是10*10矩阵,但是除了常量5和10,两个函数的其它部分完全相同。这会引出代码膨胀。
下面是对SquareMatrix的第一次修改:
template <typename T>
class SquareMatrixBase{  //与尺寸无关的基类
protected: //避免derived classes代码重复

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:

//制造inline,调用base版的invert
//不使用this->,就会使模板化基类内的函数名称会被derived类掩盖
void invert(){return this->invert(n);};
};
示例9-4-2  正方矩阵建立带数值参数的函数
但是还有一个问题没有解决,SquareMatrixBase<T>::invert如何知道哪个特定矩阵的数据在哪儿?一个方法是令SquareMatrixBase存储一个指针,指向矩阵数值所在的内存。
template <typename T>
class SquareMatrixBase{  //与尺寸无关的基类
protected: //避免derived classes代码重复
SquareMatrixBase(std::size_t n, T*pMem):size(n),pData(pMem){}

void invert(std::size_t matrixSize); //以给定的尺寸求逆矩阵
private:
std::size_t size;  //矩阵大小
T* pData;    //指针,指向矩阵数据
};
// derived classes之一
template <typename T, std::size_t  n>
class SquareMatrix::private SquareMatrixBase<T>{
private:
using SquareMatrixBase<T>:: invert; //避免这样base版的invert,条款33
T data[n*n];
public:
SquareMatrix():SquareMatrixBase<T>(n,data){}

//制造inline,调用base版的invert
//不使用this->,就会使模板化基类内的函数名称会被derived类掩盖
void invert(){return this->invert(n);};
};
// derived classes之二
template <typename T, std::size_t  n>
class SquareMatrix::private SquareMatrixBase<T>{
private:
using SquareMatrixBase<T>:: invert; //避免这样base版的invert,条款33
boost::scoped_array<T> pData;   //关于boost::scoped_array,条款13
public:
SquareMatrix():SquareMatrixBase<T>(n,0),  //将base class的数据指针设为NULL
pData(new T[n*n])      //将指向矩阵内存的指针存储起来
{ this->setDataPtr(pData.get());}  //将它的一个副本交给base class

//制造inline,调用base版的invert
//不使用this->,就会使模板化基类内的函数名称会被derived类掩盖
void invert(){return this->invert(n);};
};
示例9-4-3  正方矩阵求逆运算函数的与尺寸有关版本
此例硬是绑着矩阵尺寸的那个invert版本,有可能生成比共享版本更佳的代码。
从另一个角度看,不同大小的矩阵只拥有单一版本的invert,可减少指向文件大小,也就是降低程序的working set大小,并强化指令高速缓存的引用集中化,提高了效率。
另一个关心的主题是对象大小。如果你不介意,可将前述“与矩阵大小无关的函数版本”搬至base class内,这会增加每一个对象的大小。
Template生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数发生相依关系。
因非类型模板参数而造成的代码膨胀可以消除,做法是以函数参数或class成员变量替换template参数。

因类型而造成的代码膨胀,往往可以降低,做法是让带有完全相同二进制表述的具现类型共享实现吗。例如,如果你实现某些成员函数而它们的操作强型指针(strongly typed pointers,T*),你应该令他们调用另一个操作无类型指针(untyped pointers,void*)。某些C++STL实现版本的确为vector,deque和list等template做了这件事。


9.5 条款45:运用成员函数模板介绍所有兼容类型 (Use member function templates to accept "all compatible types")

所谓智能指针(Smart pointers)是“行为像指针”的对象,并提供指针没有的机能。真实指针做的很好的一件事,支持隐式转换(implicit conversions)。
如果想在用户自定的智能指针中模拟Derived class指针可以隐形转换为base class指针,或“指向non-const对象”的指针转为“指向const对象”等行为,如下:

class Top {…};
class Middle:public Top {…};
class Bottom:public Middle {…};
Top* p1 = new Middle;
Top* p2 = new Bottom;
const Top* pc2 = pt1;

template <typename T>
class SmartPtr{  
public:
template<typename U>
SmartPtr(const SmartPtr<U> & other):heldPtr(other.get())
{…}; //member template copy构造函数
T* get const{return heldPtr;};

private:
T* heldPtr;  //这个SmartPtr持有的内置原始指针
};
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Middle>(new Bottom);
SmartPtr<const Top> pt2 = pt1;
示例9-5-1  泛化copy构造函数
此例,我们是为了写一个构造目标,这样的模板是所谓member function templates,作用是为class生成函数。对于任何类型T和任何类型U,这里可以根据SmartPtr<U>生成一个SmartPtr<T>,因为SmartPtr<T>有个构造函数接收一个SmartPtr<U>参数。
我们使用成员初始列来初始化SmartPtr<T>之内类型为T*的成员变量。并以类型为U*的指针作为初值。这个行为只有当“存在某个隐式转换可将一个U*指针转为一个T*指针”时才能通过编译。
Member function templates的效用不限于构造函数,他们常扮演的另一个角色是支持赋值操作。参见shared_ptr的实现。
在class内声明泛化copy构造函数(一个template)并不会阻止编译器生成他们自己的copy构造函数(一个non-template)。
Member function templates(成员函数模板)生成可以接受所用兼容类型的函数。
如果你声明了Member templates用于泛化copy构造函数或泛化assignment操作,你还是需要声明正常的copy构造函数和copy assignment操作符。

9.6 条款46:需要类型转换时请为目标定义非成员函数 (Define non-member functions inside templates when type conversions are desired)

条款24讨论了为什么唯有non-member函数才有能力“在所有实参身上实施隐式类型转换”,该条款并以Rational class的operator*函数为例。本条款将Rational和operator*模板化了:

template<typename T>
class Rational {  
public:
Rational (const T& numerator = 0, //条款20,pass by reference
const T& denominator = 1);
       const T numerator() const; //条款28,pass by vaule
       const T denominator() const; //条款3,返回const
};
template<typename T>
const Rational<T> operator*(const Rational<T> &lhs,
const Rational<T> &rhs);
{
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator ()*rhs.denominator());
}
Rational<int> oneHalf(1,2);  //a =1/2
Rational<int> result;
  result = oneHalf * 2;         //错误无法通过编译
示例9-6-1  Rational类的模板化
上述失败的启示是,模板化的Rational内的某些东西似乎和其non-template版本不同,这里编译器试图具现化某个“名为operator*并接受两个Rational<T>参数”的函数,但是不知道T是什么。原因在于,function template实参推到过程中从不将隐式类型转换函数纳入考虑,也就是没有考虑通过构造函数而发生的隐式转换。
Template class内的friend声明式可以指涉某个特定函数,这就意味class Rational<T>可以声明为operator*是它的一个friend函数。Class template并不依赖function template实参推导,所以编译器总能在class Rational<T>具现化时得知T。因此,令Rational<T> class声明适当的operator*为其friend函数:
template<typename T>
class Rational {  
public:
Rational (const T& numerator = 0, //条款20,pass by reference
const T& denominator = 1);
       const T numerator() const; //条款28,pass by vaule
       const T denominator() const; //条款3,返回const

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);
{
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator ()*rhs.denominator());
}

Rational<int> oneHalf(1,2);  //a =1/2
Rational<int> result;
  result = oneHalf * 2;         //通过编译,但不能链接
示例9-6-2  Rational类声明friend函数
在一个class template内,template名称可被用来作为“template和其参数”的简略表达方式,所以在Rational<T>内我们可以只写Rational而不必写Rational<T>。
template<typename T>
class Rational {  
public:
Rational (const T& numerator = 0, //条款20,pass by reference
const T& denominator = 1);
       const T numerator() const; //条款28,pass by vaule
       const T denominator() const; //条款3,返回const

friend const Rational operator*(const Rational &lhs,
const Rational &rhs);
{
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator ()*rhs.denominator());
}
};
template<typename T>
const Rational<T> operator*(const Rational<T> &lhs,
const Rational<T> &rhs);
{
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator ()*rhs.denominator());
}

Rational<int> oneHalf(1,2);  //a =1/2
Rational<int> result;
  result = oneHalf * 2;         //通过编译也能链接
示例9-6-3  Rational类定义friend函数
当我们编写一个class template,而他所提供之“与此template相关的”函数支持“所有参数之隐式转换”时,请讲那些函数定义于“class template内部的friend函数”。

此时friend函数的用途不再是访问class的non-public成分,而是让类型转换可能发生在所有实参身上。


9.7 条款47:请使用traits classes表现类型信息 (Use traits classes for information about types)

STL主要由“用以表现容器、迭代器和算法”的templates构成,但也覆盖若干工具,如advance,用来将某个迭代器移动某个给定距离:
template<typename IterT, typename DistT>
void advance(IterT &iter, DistT d);  //如果d<0,则向后移动
对于STL的5种迭代器专属的卷标接口(tag struct),如下:
struct input_iterator_tag{};
struct output_iterator_tag{};
struct forward_iterator_tag:public input_iterator_tag {};
struct bidirectinal_iterator_tag: public forward_iterator_tag {};
struct random_access_iterator_tag: public bidirectinal_iterator_tag {};
这些structs之间的继承关系是有效的is-a关系。Advance的实现
template<typename IterT, typename DistT>
void advance(IterT &iter, DistT d);
{
    if (iter is random access iterator){
        iter+=d;
}
    else{
    if (d > =0) { while (d--) ++iter;}
    else { while (d++) --iter;}
}
}
示例9-7-1  Advance的实现
这种做法需要知道IterT是否为random access迭代器分类。而Traits技术允许你在编译期间取得某些类型信息。这个技术的要求之一,它对内置类型和用户自定义类型的表现必须一样好。比如,上述advance收到的实参是一个指针和一个int,advance仍然必须有效运作。但是它,排除了类型内的嵌套信息。因此,类型的traits信息必须位于类型自身之外。标准技术是把它放进一个template及其多个特化版本中。下面template用来处理迭代器分类的相关信息:
template<typename IterT >
struct  iterator_traits;
这里的iterator_traits是个struct,又往往被称为traits classes。它的运作方式是,针对每一个类型IterT,在struct iterator_traits<IterT>内声明某个typedef名为iterator_category。这个typedef用确认IterT迭代器分类。例如:

template<…>
class deque {
public:
class iterator {
    typedef random_access_iterator_tag iterator_category;
};
};
template<…>
class list {
public:
class iterator {
    typedef bidirectinal_iterator_tag iterator_category;
};
};
template<typename IterT>
struct  iterator_traits {
    typedef  typename IterT::iterator_category iterator_category;
};
示例9-7-2 iterator_traits第一部分实现
此例iterator_traits对于用户自定义类型行得通,但是对指针行不通,因为指针不可能嵌套typedef。


template<typename IterT>
struct  iterator_traits< IterT *> {
    typedef  random_access_iterator_tag iterator_category;
};
示例9-7-2 iterator_traits第二部分实现
如何设计并实现traits class:
1)    确认若干你希望将来可取得的类型相关信息,例如对于迭代器而言,希望取得分类(category)。
2)    为该信息选择一个名称,例如iterator_category。
3)    提供一个template和一组特化版本,内含你希望支持的类型相关信息。
好了,我们对advice实践先前的为嘛:
template<typename IterT, typename DistT>
void advance(IterT &iter, DistT d);
{
    if (typeid(typname std::iterator_traits<IterT>::iterator_category)
== typeid(std::random_access_iterator_tag)){
}
。。。
}
IterT类型在编译期间获知,所以std::iterator_traits<IterT>::iterator_category也可在编译期间确定。
   把条件式改成重载:
template<typename IterT, typename DistT>
void doAdvance(IterT &iter, DistT d, std::random_access_iterator_tag);
{
   iter +=d;
}
void doAdvance(IterT &iter, DistT d, std::bidirectinal_iterator_tag);
{
 if (d > =0) { while (d--) ++iter;}
else { while (d++) --iter;}
}
void doAdvance(IterT &iter, DistT d, std::input_iterator_tag);
{
 if (d < 0) {thow std::out_of_range(“Negative distance”);}
else { while (d++) --iter;}
}
template<typename IterT, typename DistT>
void advance(IterT &iter, DistT d);
{
    doAdvance(
iter, d,   // 对iter之迭代器分类而言
typename  //必须的
std::Iterator_traits<IterT>::iterator_category());
}
示例9-7-3  Advance的再次实现
总结如何使用一个traits class了:
1)    建立一组重载函数(身份像劳工)或函数模板(例如doAdvance),彼此之间差异在于各自的traits参数。
2)    建立一个控制函数(身份像工头)或函数模板(例如advance),它调用哪些劳工函数并传递traits class所提供的信息。

9.8 条款48:认识template元编程 (Be aware of template metaprogramming)

Template metaProgramming(TMP,模板元编程)是编写template-based C++程序并执行于编译期的过程。所谓template metaprogram(模板元程序)是以C++写成、执行于C++编译器内的程序。
Template metaProgramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦查和更高的效率。
TMP可被用来生成“基于政策选择组合”(base on combination of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。
其他见《Effective C++ 中文 第三版》P233。