- 条款41了解隐式接口和编译期多态
- 条款42了解typename的双重意义
条款41:了解隐式接口和编译期多态
面向对象编程总是以显示接口(explicit interfaces)和运行期多态(runtime polymorphism)来解决问题。例如
class Widget{
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
……
};
void doProcessing(Widget& w)
{
if(w.size()>10 && w!=someNasyWidget)
{
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
可以这样说doProcessing
内的w
- w的类型被声明为Widget,所以w必须支持Widget接口。
- Widget的某些成员函数是virtual,w对于这样函数的调用将表现出运行期多态(runtime polymorphism)。
在Templates和泛型编程的世界中,显示接口和运行期多态仍然存在,但是更要到的是隐式接口(implicit interface)和编译器多态(compile-time polymorphism)。将doProcessing
从函数变为函数模板(function template)
template<typename T>
void doProcessing(T& w)
{
if(w.size()>10 && w!=someNasyWidget)
{
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
现在再来看doProcessing
内的w
- w必须支持哪种接口,有template中执行于w身上的操作来决定。
- 凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template的具现化(instantiated),使这些调用得以成功。这样的局现化发生在编译期。以不同template参数具现化function template会导致调用不同的函数,这就是编译期多态(compile-time polymorphism)。
编译期多态和运行期多态不难区分,现在来看显式接口和隐式接口的区别。通常显式接口有函数的签名式(函数名称、参数类型、返回值)构成。
class Widget{
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
……
};
public接口由一个构造函数、一个析构函数、函数size,normalize、swap以及其参数、返回值、常量性(constness)构成,还包括编译器产生的copy构造函数和copy assignment操作符。
隐式接口就完全不同,它不是由函数签名决定,而是由有效表达式(valid expression)组成。
template<typename T>
void doProcessing(T& w)
{
if(w.size()>10 && w!=someNasyWidget)
{
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
可以看出T(w类型)的隐式接口好像有这些约束
- 必须提供一个名为size的函数,该函数返回一个整数值
- 必须支持一个operator!=汗还是,用来比较两个对象。
其实并不是必须满足这两个约束。T必须支持size成员函数,但是这个函数可能从base class继承。这个函数不需要返回一个整数值,甚至不需要返回一个数值类型。甚至不需要返回一个定义有operator>的类型。它唯一要做的就是返回一个类型为X的对象,而X对象加上一个int(10的类型)必须能够调用一个operator>。这个operator>不需要非得取得一个类型为X的参数,它可以取得类型为Y的参数,只要存在一个隐式转换能够将类型X的对象转换为类型为Y的对象。同理T不需要支持operator!=。
以上分析还没有考虑operator&&被重载,一个连接词的改变或许完全不同的某种东西,可能改变上述表达式的意义。
第一次以此种方式思考隐式接口会感觉不习惯。隐式接口仅仅由一组有效表达式构成,这个表达式可能看起来很复杂,但它们要求的约束条件一般而言相当直接又明确,例如:
if(w.size()>10 && w!=someNasyWidget)
关于函数size、operator>、operator&&、operator!=身上的约束条件,很难再说太多;但整体确认表达式约束条件很容易。if表达式必须为布尔值,因此整体表达式必须与bool兼容。这时template doProcessing中类型参数T隐式接口的一部分,doProcessing要求其他隐式接口:copy构造函数、normalize和swap也必须对T型对象有效。
加诸于template参数身上的隐式接口和加诸于class对象身上接口一样真实,都是在编译期完成检查,如果template中使用不支持template所要求的隐式接口,代码不能编译通过
总结
- class和template都支持接口和多态。
- class的接口是显式的,以函数签名为中心,多态是通过virtual函数发生于运行期。
- template的接口是隐式的,基于有效表达式。多态是通过template具体化和函数重载解析(function overloading resolution)发生在编译期。
条款42:了解typename的双重意义
使用模板时,可以用typename,也可以用class
template<class T> class Widget;
template<typename T> class Widget;
两者没有什么不同。作为template的类型参数,意义完全相同。在使用习惯上来说,很多人喜欢使用typename,因为这暗示参数并非一定要是个class类型。
C++有时不会把class和typename等价。有时候一定要用typename;为了了解原理,先来谈谈在template内指涉(refer to)的两种名称。
用一个例子来说明,现在有个template function,接收STL兼容容器为参数,容器内对象可被赋值为int,这个函数打印第二个元素的值
template<typename C>
void print2nd(const C& container)
{
if(container.size()>=2)
{
C::const_iterator iter(container.begin());
++iter;
int valaue=*iter;
std::cout<<value;
}
}
现在解释一下两个local变量iterate和value。iterate类型为C::const_iterator,实际是什么取悦于template参数C。template内出现的名称如果依赖于某个template参数,称这个名称为从属名称(dependent names)。如果从属名称在class内呈嵌套状,称之为嵌套从属名称(nested dependent name)。C::const_iterator就是这样的嵌套从属名称(nested dependent name),且指涉某类型。value是int类型,不依赖template参数,称之为非从属名称(non-dependent names)。
嵌套从属名称有可能导致解析(parsing)困难,例如在print2dn这样做
template<typename C>
void print2nd(const C& container)
{
C::const_iterator* x;
……
}
表面上看是声明了一个local变量x,x类型是只需C::const_iterator的指针,这是因为我们直到C::const_iterator是个类型。如果不是这样呢?例如,C内有个static成员变量碰巧命名为const_iterator,或x是个global变量;这样的话就是一个相乘动作:const::const_iterator乘以x。编写C++解析器的人必须操心这样的输入。
在直到C是什么之前,不能确定C::cont_iterator是否为一个类型。当编译器解析template print2nd时,不知道C是什么东西。C++有个规则可以解析这个歧义状态:如果解析器在template中遭遇到一个嵌套从属名称,它便假设这个明白不是个类型,除非你告诉它是。所以缺省情况下,嵌套从属名称不是类型。这个规则还有个例外,稍后再提。
再来看看print2nd,C::const_iterator iter
只有在C::const_iterator
是个类型时才合理,但是C++缺省认为它不是。要矫正这个形式,我们必须告诉C++C::const_iterator
是个类型,在其前面放置关键字typename即可
template<typename C>
void print2nd(const C& container)
{
if(container.size()>=2)
{
typename C::const_iterator iter(container.begin());
++iter;
int valaue=*iter;
std::cout<<value;
}
}
“typename必须作为嵌套从属类型名称的前缀词”这一规则的例外是:typename不可以出现在base class list内的嵌套从属类型名称之前,也不可以在member initialization 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) //mem init.list中不允许typename
{
typename Base<T>::Nested temp; //嵌套从属名称类型
…… //既不在base class list中,也不再mem init list中,作为base class修饰符要加上typename
}
……
};
再来看一个typename例子。一个function template接受一个迭代器,我们打算为该迭代器指涉的对象做一份local复件(副本)temp
template<typename IterT>
void workWithIterator(IterT iter)
{
typename::std::iterator_traits<IterT>::value_type temp(*iter);
……
}
typename::std::iterator_traits<IterT>::value_type
是标准traits class(**条款**47)的一种运用。std::iterator_traits<IterT>::value_type
是个嵌套从属类名称,所以在其前面放置typename。如果使用std::iterator_traits<IterT>::value_type
感觉太长不习惯,可以考虑建立一个typedef,对于traits成员名称,普遍的习惯是设定typedef名称代表某个traits成员名称。
template<typename IterT>
void workWithIterator(IterT iter)
{
typedef typename::std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
……
}
最后提一下,typename相关规则在不同编译器上有不同的实践。这意味着typename和嵌套从属名称之间的互动,也许会在移植性方面带来头疼的问题。
总结
- 声明template参数时,前缀关键字class和typename可以互换。
- 使用关键字typename标识嵌套从属类型名称;但不得在base class list(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。