条款45、运用成员函数模板接受所有兼容类型
所谓智能指针是“行为像指针”的对象,并提供指针没有的机能。
真实指针做的很好的一件事情就是,支持隐式转换。继承类指针可以隐式转换为基类指针,指向non-const的对象可以转换为指向const的对象等等。
但是,同一个template的不同具现体之间并不存在什么与生俱来的固有关系,比如说,带有derived-base关系的B,D两类型分别是具现化某个template,产生出来的两个具现体并不带有derived-base关系。
就原理而言,因为一个template可以被无限量具现化,以致生成无限量函数,因此我们似乎不能为一个模板写一个构造函数,而要为它写一个构造模板。这样的模板就是所谓的member function template(常简称为member template),其作用就是为class生成函数。
1、使用member function template(成员函数模板)生成“可接受所有兼容类型”的函数
template<typename T>
class SmartPtr{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other);//member template,为了生成泛化copy构造函数
...
};
以上代码的意思是,对于任何类型T和任何类型U,这里可以根据SmartPtr<U>生成一个SmartPtr<T>,U和T的类型是同一个template的不同具现化,有时候我们称为泛化copy构造函数。
我们可以在构造模板实现代码中约束转换行为,使它为我们提供更多的功能:
template<typename T>
class SmartPtr{
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other):heldPtr(other.get()){...}
T* get() const{return heldPtr;}
...
private:
T* heldPtr;
...
};
member function template的效用不限于构造函数,他们常扮演的另一个角色是支持赋值操作。
template<class T>
class shared_ptr{
public:
template<class Y>
explicit shared_ptr(Y* p);//构造,来自任何兼容的内置指针
template<class Y>
shared_ptr(shared_ptr<Y> const& r);//构造,来自任何兼容的shared_ptr
template<class Y>
explicit shared_ptr(weak_ptr<Y> const& r);//构造,来自任何兼容的weak_ptr
template<class Y>
explicit shared_ptr(auto_ptr<Y>& r);//构造,来自任何兼容的auto_ptr
template<class Y>
shared_ptr& operator=(shared_ptr<Y>const& r);//赋值,来自任何兼容的shared_ptr
template<class Y>
shared_ptr& operator=(auto_ptr<Y>& r);//赋值,来自任何兼容的auto_ptr
...
};
以上所有构造函数中,只有shared_ptr(shared_ptr<Y> const& r)没有explicit,说明从某个shared_ptr隐式转换至另一个shared_ptr类型是被允许的,但其他不被允许进行隐式转换。另一点是,trl::shared_ptr构造函数和赋值操作符的trl::auto_ptr并未被声明为const传递,那是因为这两个行为中指针被改动了。
2、如果你声明member template用于“泛化copy构造函数”或“泛化assignment操作”,你还需要声明正常的copy构造函数和assignment操作符。
因为member function template并不改变语言规则。在类内声明“泛化copy构造函数”或“泛化assignment操作”并不会阻止编译器生成他们自的的copy构造函数和assignment操作符。
条款46、需要类型转换时请为模板定义非成员函数
问题现象:模板化的template内的某些东西似乎和non-template版本不同,和条款24相同的代码,但是本条款将Rational和operator*模板化了。
在条款24中,我们讨论过只有non-member函数才有能力“在所有实参身上实施隐式类型转换”,编译器知道我们尝试调用什么函数。在本条款的模板化中,在这里编译器不知道我们想要调用哪个函数。
template<typename T>
class Rational{
public:
Rational(const T& numerator=0,const T& denominator=1);
const T numerator() const;
const T denominator() const;
...
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs){...}
//调用
Rational<int> onehalf(1,2);
Rational<int> result=onehalf*2;//错误!无法通过编译
原因:在template实参推导过程中从不将隐式类型转换函数纳入考虑。
解决方法:为了让类型转换可能发生在所有实参身上,我们需要一个non-member函数(条款24);为了令这个函数被自动具现化,我们需要将它声明在class内部;而在class内部声明non-member函数的唯一办法就是令它成为一个friend函数;但是仅仅这样代码只能通过编译却无法连接,为了连接器能找到这个函数,我们就需要将这个函数定义在class内,让声明式与定义是合并。
template<typename T>
class Rational{
public:
...
friend const Rational operator*(const Rational& lhs,const Rational& rhs)
{
return Rational(lhs.numerator()*rhs.numerator(),lhs.denomerator()*rhs.denomerator());
}
};
/*在一个class template内,template名称可被用来作为“template和其参数”的简略表达方式,所以在rational<T>内我们可以只写Rational而不必写Rational<T>,在参数名称很长的时候,这可以节省时间,让代码比较干净,如果他被声明如下,一样有效
const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs){...}*/
定义于class内部的函数都暗自成为inline,包括像operator*这样的friend函数。如果你想将复杂函数所带来的inline冲击最小化,做法是让它不做任何事情,只调用一个定义于class外部的辅助函数。
template<typename T> class Rational;
template<typename T>
const Rational<T> deMultiply(const Rational<T>& lhs,const Rational<T>& rhs)
{
return Rational<T>(lhs.numerator()*rhs.numerator(),lhs.denomerator()*rhs.denomerator());
}
template<typename T>
class Rational{
public:
...
friend const Rational operator*(const Rational& lhs,const Rational& rhs)
{
return doMultiply(ihs,rhs);
}
};
作为一个template,doMultiply不支持混合式乘法,但它也不需要,它在被operator*调用,而operator*支持混合式乘法操作!本质上operator*支持了类型转换所需要的任何东西,确保两个Rational对象能够相乘,然后将这两个对象传给一个适当的doMultiply template具现化,完成实际的乘法操作,协作成功。
总结:当我们编写一个class template,而她所提供的“与此template相关的”函数支持“所有参数的隐式类型转换”时,请将那些函数定义为“class template内部的friend函数"。
条款47、请使用traits classes表现类型信息
STL主要由“用以表现容器、迭代器和算法”的template构成,但是也覆盖若干工具性的templates,其中一个名为advance ,用来将某个迭代器移动某个给定的距离
template<typename IterT,typename DistT>
void advance(IterT& iter,DistT d);
观念上advance只做iter+=d动作,但是实际上只哟random access(随机访问)迭代器才支持+=操作。首先重温下 STL迭代器的分类:
input迭代器:只能向前移动,一次一步,客户只可以读取而不能改写他们所指的东西,而且只能读一次。C++程序库中istream_iterators是这一类的代表。
output迭代器:只能向前移动,一次一步,客户只能改写他们所指的东西,而且只能写一次。C++程序库中ostream_iterators是这一类的代表。
forward迭代器:这种迭代器可以做前述两种迭代器所做的每一件事,而且可以读或写其所指物一次以上,他可施行于多次性 操作算法。
bidirectional迭代器:它可以向前或向后移动,STL的list迭代器就属于这一分类,set,multiset,map,multimap也都是这一分类。
random access迭代器:是最有威力的迭代器,他可以在在常量时间内向前或向后跳跃任意距离,这种算术类似于指针算术,所以内置指针也可以被当做random access迭代器使用。vector,deque,string提供的迭代器都是这一分类的。
我们希望能够根据不同类型的迭代器提供不同的算术运算,来提高代码的效率。这样就需要首先判断迭代器的分类,获得迭代器的类型信息。那就是traits让你得以进行的事:他们允许你在编译期间获得某些类型信息。
traits并不是C++关键字或一个预先定义好的构件,它们是一种技术,也是一个C++程序员共同遵守的协议。这个技术的要求之一是:它对内置类型和用户自定义类型表现必须一样好。
“traits 能够施行于内置类型”意味着“类型内的嵌套信息”这种东西出局,类型的traits信息必须位于类型自身之外。标准技术将它放进一个template及其一或多个特化版本中,这样的template在标准程序库中有若干个,其中针对迭代器者被命名为iterator_traits。
template<typename IterT>
struct iterator_traits;//template用来处理迭代器分类的相关信息,习惯上traits总是被实现为struct,但是往往被称为traits classes
iterator_traits的运作方式是,针对每一个类型iterT,在struct iterator_traits<IterT>内声明某个typedef名为iterator_category。这个typedef用来确认IterT的迭代器分类。
如下:
template<typename IterT>
class deque{
public:
class iterator{
public:
typedef random_access_iterator_tag iterator_category;
...
};
...
};
template<typename IterT>
class list{
public:
class iterator{
public:
typedef bidirectional_iterator_tag iterator_category;
...
};
...
};//首先每一个用户自定义的迭代器类型必须嵌套一个typedef名为iterator_category,用来确认适当的卷标结构(tag struct)
template<typename IterT>
struct iterator_traits{
typedef typename IterT::iterator_category iterator_category;
...
};//iterator_traits响应iterator class的嵌套式typedef
//类型IterT的iterator_category其实是用来表现”IterT说它自己是什么"
这对用户自定义类型行得通,但对指针(也是一种迭代器)行不通,因为指针不可能嵌套typedef。为了支持指针迭代器,iterator_traits特别针对指针类型挺一个偏特化版本。由于指针的行径与random access迭代器类似,所以iterator_traits为指针指定的迭代器类型是:
template<typename IterT>
struct iteratpr_traits<IterT*>//template偏特化针对内置指针
{
typedef random_access_iterator_tag iterator_category;
...
};
总结:如何设计并实现一个traits class:
1.确认若干你希望将来可取得的类型相关信息。例如对迭代器而言,我们希望将来可取得其分类。
2.为该信息选择一个名称(例如iterator_category)
3.提供一个template和一组特化版本(例如iterator_traits),内含你希望支持的类型相关信息
traits class使得“类型相关信息”在编译期可用。它们以templates和“template特化”完成实现。
好,现在有了iterator_traits,我们可以对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))
...
}
虽然这看起来正确,但是并非是我们想要的。首先他会导致编译问题。IterT类型在编译期间获知,但 if语句确是在运行期才会核定。这不仅浪费时间,也会造成可执行文件膨胀。
整合重载技术后,traits class有可能在编译期对类型执行if else测试。为了让advance的行为如我们所愿,我们需要做的是产生重载版本,内含advance的本质内容,但各自接受不同类型的iterator_category对象,我们将这个函数取名为doAdvance:
template<typename IterT,typename DistT)
void doAdvance(IterT& iter,DistT d,std::random_access_iterator_tag)
{
iter+=d;
}
template<typename IterT,typename DistT)
void doAdvance(IterT& iter,DistT d,std::bidirectional_iterator_tag)
{
if(d>=0){while(d--) ++iter;}
else{while(d++) --iter;}
}
template<typename IterT,typename DistT)
void doAdvance(IterT& iter,DistT d,std::input_iterator_tag)
{
if(d<0){
throw std::out_od_range("Negative distance");
while(d--) ++iter;
}
template<typename IterT,typename DistT>
void advance(IterT& iter,DistT d)
{
doAdvance(iter,d,typename std::iterator_traits<IterT>::iterator_category());
}//调用的doAdvance版本对iter的迭代器分类而言必须是适当的
现在我们可以总结如何使用一个traits class 了:
- 建立一组重载函数(身份像劳工)或函数模板(例如doAdcance),彼此间的差异只在于各自的traits参数。令每个函数实现码与其接受的traits信息相应和
- 建立一个控制函数(身份像工头)或函数模板(例如advance),它调用上述哪些“劳工函数”并传递给traits class所提供的信息