条款41:了解隐式接口和编译期多态
面向对象编程总是以显示接口和运行期多态解决问题。template及泛型编程的世界,与面向对象有根本不同,虽然显示接口和运行期多态仍然存在,但重要性降低,而隐式接口和编译期多态更为重要。
运行期多态和编译期多态类似于“哪一个重载函数被调用”(编译期)和“哪一个virtual函数该被绑定”(运行期)的差异类似。
显示接口由函数签名式(名称、参数类型、返回类型)构成,隐式接口不基于函数签名式,而是由函数内部有效表达式所决定的。
加诸于template参数身上的隐式接口,就像加诸于class对象身上的显示接口一样真实,而且两者都在编译期完成检查。就像你无法以一种“与class提供的显示接口矛盾”的方式来使用对象,你也无法在template中使用“不支持template所要求的隐式接口”的对象。
请记住:
class和template都支持接口和多态
对class而言接口是显示的,以函数签名为中心,多态则是通过virtual函数发生于运行时期
对template参数而言,接口是隐式的,奠基于有效表达式,多态则是通过template具现化和函数重载解析发生于编译期
条款42:了解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;
}
}
分析上述打印STL容器第二个元素值的函数为何不能通过编译。template内出现的名称如果依赖于某个template参数,则称之为从属名称,如果从属名称在class内呈嵌套状,称之为嵌套从属名称。C::const_iterator就是这样一个名称。实际上它还是一个嵌套从属类型名称,也就是嵌套从属名称并且涉指某各类。对于value,其类型是int,称为非从属名称。
嵌套从属名称可能导致解析困难,我们可以在函数开头编写:
C::const_iterator *x;
看起来似乎是声明了一个C::const_iterator的指针,之所以这么认为,是由于我们知道C::const_iterator是一个类型,但是如果C中有一个static成员变量名称是const_iterator,上述代码就是变为相乘操作。并且解析器自动将嵌套从属名称假设为非类型。
因此任何时候当想要在template中涉指一个嵌套从属类型名称,必须在紧邻它的前一个位置放上关键字typename:
template<typename C> void print2nd(const C &container)
{
if(container.size()>=2)
{
typename C::const_iterator iter(container.begin());
...
}
}
typename只被用来验明嵌套从属类型名称,这一规则的例外是:不可出现在base class list内的嵌套从属类型名称之前,也不可在member initialize list(成员初始值列)中作为base class修饰符:
template<typename T> class Derived
:public Base<T>::Nested{ //base class list中不允许“typename”
public:
explicit Derived(int x)
:Base<T>::Nested(x) //member initialization list中不允许“typename”
{
typename Base<T>::Nested temp; //嵌套从属类型名称允许“typename”
...
}
};
请记住:
声明template参数时,前缀关键字class和typename可互换
请使用关键字typename标识嵌套从属类型名称;但不可以在base class list或member initialization list内作为base class修饰符
条款43:学习处理模板化基类内的名称
template<typename Company> class MsgSender{
public:
void sendClear(...){...}
void sendSecret(...){...}
};
template<typename Company> class LoggingMsgSender:public MsgSender<Company>{
public:
void sendClearMsg(...)
{
...
sendClear() //调用基类函数,此处不能通过编译
...
}
};
之所以不能通过编译,是由于编译器 不知道LoggingMsgSender继承的是什么样的class,因为base class是一个template class,编译器不知道该base class是否有sendClear函数。例如,我们可以产生一个特化模板:
template<> class MsgSender<CompanyZ>{
public:
void sendSecret(...){...}
};
由此可见,当company==companyZ,sendClear函数将不存在,这就是为什么编译不通过的原因:base class template有可能被特化,而那个特化版本不一定会提供和一般template一样的接口。因此编译器拒绝在templatized base class(模板化基类)内寻找继承而来的名称。
为了解决“不进入templatized base classes观察”有三种办法:
- 在base class函数的调用动作之前加上this->,假设会继承基类的sendClear函数:
void sendClearMsg(...)
{
this->sendClear(...);
}
- 使用using声明:
using MsgSender<Company>::sendClear; //假设sendClear位于base class内
void sendClearMsg(...)
{
sendClear(...);
}
- 明白指出被调用的函数位于base class内:
void sendClearMsg(...)
{
MsgSender<Company>::sendClear(...);
}
以上三种办法均可解决上述问题,但第三种办法相较而言不够完美,会关闭virtual绑定行为。虽然通过编译,但如果上述承诺未被实践,编译器仍然会报错:
LoggingMsgSender<CompanyZ> zMsgSender;
zMsgSender.sendClearMsg(...); //无法通过编译
因为此时编译器已经知道base class是特化版本MsgSender<CompanyZ>,并且该特化版本不提供sendClear函数
请记住:
可在derived class templates内通过“this->”指涉base class templates内的成员名称,或藉由一个明白写出的“base class”资格修饰符完成
/* 条款44还没有很好的理解,就先略过了,后续有所领悟再回头修改 */
条款45:运用成员函数模板接受所有兼容类型
之前使用的智能指针时“行为像指针”的对象,不用担心内存泄漏、重复析构等问题,但真实指针做得更好的一件事情是,支持隐式转化:
class Top{};
class Middle:public Top{};
class Bottom:public Middle{};
Top *pt1=new Middle; //将Middle*转换为Top*
Top *pt2=new Bottom; //将Bottom*转换为Top*
const Top *pct2=pt1; //将Top*转换为const Top*
如果我们想在自定义的智能指针中模拟上述转换,我们希望以下代码通过编译:
tenplate<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的不同具现体之间并不存在任何联系。所以编译器认为Smart<Middle>和Smart<Top>为完全不同的class。我们可以编写构造函数完成不同类型之间的转换,但如果在继承体系中又有新的类型加入,我们就需要添加大量的代码,以完成转换,因此这并不是一个可实行的方法。
因此我们应该编写一个构造模板。这样的模板是所谓的member function templates(简称member templates):
template<typename T>class SmartPtr{
public:
template<typename U>SmartPtr(const SmartPtr<U> &other);
...
};
上述代码是说:任何T和U,可以根据SmartPtr<U>生成SmartPtr<T>,我们称这个函数为泛化copy构造函数。但此函数提供的东西比我们需要的更多,我们希望根据SmartPtr<Bottom>生成SmartPtr<Top>,却不希望根据SmartPtr<Top>生成SmartPtr<Bottom>,因此我们必须对所创建的成员进行筛除。
假设SmartPtr提供一个get成员函数,返回指针对象所持有的原始指针的副本,我们可以在构造函数模板实现代码中约束转换行为:
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;
};
使用类型为U*的指针初始化类型为T*的成员变量。这个行为只有当“存在某个隐式转换可以将某个U*指针转换为T*指针”时才能通过编译。
编译器会为我们产生成员函数:copy构造函数和copy assignment操作符。一旦类型T和U相同,泛化copy构造函数就会具现化为copy构造函数,那么编译器会为我们生成copy构造函数吗?
在class内部声明泛化copy构造函数并不会阻止编译器生成它们的copy构造函数。对于copy assignment操作符同样如此。
请记住:
请使用member function template(成员函数模板)生成“可接受所有兼容类型”的函数
如果你的声明member templates用于泛化copy构造或泛化assignment操作,你还是需要声明正常的copy构造函数和copy assignment操作符
条款46:需要类型转换时请为模板定义非成员函数
条款24讨论过所有实参需要隐式转换时,需要为non_member,此处我们将Rational和operator*模板化:
template<typename T>class Rational{
public:
Rational(const T &numerator=0,const T &denomirator=1);
const T numerator()const;
const T denominator()const;
...
};
template<typename T>const Rational<T> operator*(const Rational<T> &lhs,const Rational<T> &rhs);
Ratioanl<int> resualt=oneHalf*2; //错误,无法通过编译
调用函数模板的时候需要进行实参推断。operator*的第一个实参是Rational<T>,而oneHalf为Rational<int>,因此很容易推断出T为int型,但第二个实参被声明为Rational<int>,但实际类型是int。编译器如何推断T的类型?或许会期望将2通过构造函数转换为Rational<int>,但template实参推导过程中不会将隐式类型转化函数纳入考虑。
template class内的friend声明式可以涉指某个特定函数。这意味着class Rational<T>可以声明operator*是它的一个friend函数。template class并不依赖实参推导,所以编译器总能在class Rational<T>具现化时得知T:
template<typename T>class Rational{
public:
...
friend const Rational operator*(const Rational &lhs,const Rational &rhs);
};
template<typename T>const Rational<T> operator*(const Ratioanl<T> &lhs,const Ratioanl<T> &rhs)
{...}
现在operator*的混合式调用就可通过编译了,因为此时operator*不再为函数模板而是一个函数,因此可在调用时使用隐式转换函数。
如果将friend删除,即将operator*函数定义为Rational class类的成员函数,编译器又不能通过编译,因为类模板的成员函数就是函数模板(但有区别,不进行模板实参推断,而是由调用该函数的对象的类型确定,因此可以进行常规转换),参数匹配时不发生隐式转化。
上述办法虽然能解决编译问题,但却无法连接。因为虽然编译器知道要调用哪个函数,但那个函数被声明于Rational内,并没有定义,我们想在class外部定义,但是行不通——如果我们声明了一个函数,就有责任定义它,需要将定义式合并至声明式内。
我们虽然将函数声明为friend,但却与传统的friend用途“访问class的non_public成分”毫不相干。为了让类型转化施加于所有形参身上,就需要一个non_member函数;为了令这个函数被自动具现化,我们需要将它声明在class内部;而class内部声明non_member函数的唯一办法是:令它成为一个friend。
请记住:
当我们编写一个class template,而它所提供的“与此templates相关的”函数支持“所有参数的隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”
条款47:请使用traits classes表现类型信息
STL中有一个advance,用来将某个迭代器移动指定距离:
template<typename IterT,typename DistT>void advance (IterT &iter,DistT &d);
观念上advance只做iter+=d,但实际上只有random access迭代器才支持+=,而其他的只能重复++或者--操作。
STL有五种迭代器:
- input迭代器,只能向前移动,一次一步,只能读取(不可涂写)所指,并且只能读取一次
- output迭代器,只能向前移动,一次一步,只能涂写(不可读取)所指,并且只能涂写一次
- forward迭代器,可做上述两种迭代器的工作,可读或写一次以上
- bidirectional相较于forward迭代器,可以向后移动
- random access迭代器可做上述迭代器的工作,并且可以在常量时间内向前或向后跳跃任意距离,类似于内置指针
对上述五种迭代器,C++标准库提供专属的标卷结构(tag struct)加以确认:
struct input_iterator_tag{};
struct output_itertator_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{};
我们希望以一下代码实现advance:
template<typename IterT,typename DistT>void advance(IterT &iter,DistT &d)
{
if(iter is a random access iterator)
iter+=d;
else
{
if(d>0){while(d--) ++iter;}
else{while(d++) --iter;}
}
}
我们需要判断iter是否为random access迭代器,那就是traits所做的事情:它们允许你在编译期取得某些类型信息。traits并不是C++关键字或预先定义好的构件,而是一种技术,也是协议。它要求对内置类型和用户自定义类型的表现必须一样好,即traits技术必须也能施于内置类型身上。
标准做法是将它放入一个template及其一个或多个特化版本中,针对迭代器的对象被命名为iterator_traits:
template<typename IterT>struct iterator_traits; //用来处理迭代器相关分类信息
在struct iterator_traits<IterT>内声明某个typedef名为iterator_category,用来确认IterT的迭代器分类,iterator_traits对于用户自定义迭代器类型必须嵌套一个typedef名为iterator_category:
template<...>class deque{ //deque迭代器可随机访问,此处省略参数
public:
class iterator{
public:
typedef random_access_iterator_tag iterator_category;
...
};
...
};
template<...>class list{ //list迭代器可双向访问,此处省略参数
public:
class iterator{
public:
typedef bidirectional_iterator_tag iterator_category;
...
};
...
};
应用于iteraotr_traits,就响应iterator class的嵌套式typedef:
template<typename IterT>struct iterator_traits{
typedef typename IterT::iterator_category iterator_category; //针对对不同类型的IterT,typedef其所定义的iterator_category
...
};
对于指针迭代器,提供一个偏特化版本:
template<typename IterT>struct iterator_traits<IterT*>
{
typedef random_access_iterator_tag iterator_category;
...
};
有了上述定义,我们可以实现之前的伪代码:
template<typename IterT,typename DistT>void advance(IterT &iter,DistT &d)
{
if(typeid(typename std::iterator_traits<IterT>::iterator_actegory)==typeid(std::random_access_iterator_tag))
...
}
上述代码中,if条件中的语句可在编译器确定,但是if语句却是在运行期核定,这会导致文件膨胀,因此我们采用重载方法,增加一个形参,用来决定调用哪一个重载函数:
template<typename IterT,typename DistT>void doAdvance(IterT &iter,DisT &d,std::random_access_iterator_tag)
{
iter+=d;
}
template<typename IterT,typename DistT>void doAdvance(IterT &iter,DisT &d,std::bidirectional_iterator_tag)
{
if(d>0){while(d--)++iter;}
else{while(d++)--iter;}
}
template<typename IterT,typename DistT>void doAdvance(IterT &iter,DisT &d,std::input_iterator_tag)
{
if(d<0){throw std::out_of_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());
}
请记住:
Traits class使得“类型相关信息”在编译期可用。他它们以templates和“templates 特化”完成实现
整合重载技术后,traits class有可能在编译期对类型执行if...else测试
条款48:认识template元编程(此篇仅限于了解模板元编程)
template metaprogramming(TMP)执行于编译期,可将工作从运行期转移到编译期。
用TMP编写计算阶乘:
template<uunsigned n>struct Factorial{
enum{value=n*Factory<n-1>::value};
};
template<>struct Factory<0>{
enum={value=1};
};
每个Factory template具现体都是一个struct,每个struct都使用enum hack声明一个名为value的TMP变量,value用来保存当前阶乘值。
请记住:
template metaprogramming可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率
TMP可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码