面向对象变成世界总是以显式接口(explicit interfaces)和运行期多态(runtime polymorphism)解决问题。举个例子,给定这样(无甚意义)的class:
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() > 10 && w != someNastyWidget) {
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
我们可以这样说doProcessing内的w:
- 由于w的类型被声明为Widget,所以w必须支持Widget接口。我们可以在源码中找出这个接口(例如在Widget的.h文件中),看看它是什么样子,所以我称此为一个显式接口(explicit interface),也就是它在源码中明确可见。
- 由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态(runtime polymorphism),也就是说将于运行期根据w的动态类型(见条款37)决定究竟调用哪一个函数。
Templates及泛型编程的世界,与面向对象有根本上的不同。在此世界中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口(implicit interfaces)和编译期多态(compile-time polymorphism)移到前头了。若想知道那是什么,看看当我们将doProcessing从函数转变成函数模板(function template)时发生什么事:
template<typename T>
void doProcessing(T& w)
{
if (w.size() > 10 && w != someNastyWidget) {
T temp(w);
temp.normalize();
temp.swap(w);
}
}
现在我们怎么说doProcessing内的w呢?
- w必须支持哪一种接口,系由template中执行于w身上的操作来决定。本例看来w的类型T好像必须支持size,normalize和swap成员函数、copy构造函数(用来建立temp)、不等比较。我们很快会看到这并非完全正确,但对目前而言足够真实。重要的是,这一组表达式(对此template而言必须有效编译)便是T必须支持的一组隐式接口(implicit interface)。
- 凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化(instantiated),使这些调用得以成功。这样的具现行为发生在编译器。“以不同的template参数具现化function templates”会导致调用不同的函数,这便是所谓的编译器多态。
纵使你从未用过templates,应该不陌生“运行期多态”和“编译期多态”之间的差异,因为他类似于“哪一个重载函数该被调用”(发生在编译器)和“哪一个virtual函数该被绑定”(发生在运行期)之间的差异。显式接口和隐式接口的差异就比较新颖,需要更多更贴近的说明和解释。
通常显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。例如Widget class:
class Widget {
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
};
其public接口由一个构造函数、一个析构函数、函数size,normalize,swap及其参数类型、返回类型、常量性构成。当然也包括编译器产生的copy构造函数和copy assignment操作符(见条款5)。
隐式接口就完全不同了。它并不基于函数签名式,而是由有效表达式组成。再次看看doProcessing template一开始的条件:
template<typename T>
void doProcessing(T& w)
{
if (w.size() > 10 && w != someNastyWidget) {
...
T(w的类型)的隐式接口看来好像有这些约束:
- 它必须提供一个名为size的成员函数,该函数返回一个整数值。
- 它必须支持一个Opera!=函数,用来比较两个T对象。这里我们假设someNastyWidget的类型为T。
加诸于template参数身上的隐式接口,就像加诸于class对象身上的显式接口一样真实,而且两者都在编译期完成检查。就像你无法以一种“与class提供之显式接口矛盾”的方式来使用对象(代码将通不过编译),你也无法在template中使用“不支持template所要求之隐式接口”的对象(代码一样通不过编译)。
请记住
- classes和templates都支持接口和多态
- 对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期。
- 对template参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。