本文是在原文的基础上,细节之处加了自己的分析和思考,并不全是转载,但是自己原创的成分不够多, 所以性质上还是算转载,希望对大家有帮助
正文
面向对象编程总是以显式接口(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 的子类。
- Widget的某些成员函数是virtual,w对于这样函数的调用将表现出运行期多态(runtime polymorphism)。
在模板编程的世界中,显示接口和运行期多态仍然存在,但是更要到的是隐式接口(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应该支持哪些方法)
凡涉及w的任何函数调用,例如 operator>和operator!=,有可能造成template的具现化(instantiated),使这些调用得以成功。这样的局现化发生在编译期。以不同template参数具现化 function template会导致调用不同的函数,这就是编译期多态(compile-time polymorphism)。
通常显式接口有函数的签名式(函数名称、参数类型、返回值)构成。
public接口由一个构造函数、一个析构函数、函数size,normalize、swap以及其参数、返回值、常量性(constness)构成,还包括编译器产生的copy构造函数和copy assignment操作符。
再看一遍这个代码
class Widget{
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
……
};
隐式接口和面向对象编程完全不同,它不是由函数签名决定,而是由有效表达式(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>。 (类似于重载运算符 bool operator>(X, int );)
这个operator>不需要非得取得一个类型为X的参数,它可以取得类型为Y的参数,只要存在一个隐式转换能够将类型X的对象转换为类型为Y的对象。
同理T不需要支持operator!=,只要在调用这个方法的命名空间,有重载运算符 bool operator!=(T, someNasyWidget的类型 ) 即可。
以上分析还没有考虑operator&&被重载,一个连接词的改变或许完全不同的某种东西,可能改变上述表达式的意义。
第一次以此种方式思考隐式接口会感觉不习惯。隐式接口仅仅由一组有效表达式构成,这个表达式可能看起来很复杂,但它们要求的约束条件一般而言相当直接又明确,例如:
if(w.size()>10 && w!=someNasyWidget)
关于函数size、operator>、operator&&、operator!=身上的约束条件,很难再说太多;但整体确认表达式约束条件很容易。if表达式必须为布尔值,因此整体表达式必须与bool兼容。这是template doProcessing中类型参数T隐式接口的一部分,doProcessing要求其他隐式接口:
- copy构造函数(更准确的说是,Widget必须有一个构造函数,接受T类型的参数,否则就会编译失败 )
- Wight的swap也必须对T型对象有效。
加诸于template参数身上的隐式接口和加诸于class对象身上接口一样真实,都是在编译期完成检查,如果template中使用不支持template所要求的隐式接口,代码不能编译通过。
所以,所以我们可以得到一个结论, C++ 模板编程过于灵活,既支持面向过程编程,又支持操作符重载,给程序带来了极大的灵活性,同时也是不确定性,如果不仔细考虑,可能导致程序执行出现未定义行为的情况,所以我们要遵守一些开发规范,来约束自己的编程习惯,从而降低这些事的可能性。当然,虽然概率比较低,但是我们应该严格要求自己。
最后总结
面向对象编程(特指以继承为核心)和template都支持接口和多态。
面向对象编程的接口是显式的,以函数签名为中心,面向对象的多态是通过virtual函数发生于运行期。
template的接口是隐式的,基于有效表达式。模板的多态是通过template具体化和函数重载解析(function overloading resolution)发生在编译期。其中表达式运算符的解读的多种可能 给模板编程带来了极大的不确定性!!!
还有最后一个问题,模板函数的重载如何考量? 模板函数能不能同名同重载的问题?之所以这么问,是因为对于模板而言,类型是不确定的。
答案是:首先,模板函数的出现,本身就为了消除多类型的重载,所以模板函数不存在针对类型的重载,所谓的模板函数重载,只可能针对函数数量进行重载。
参考
[1]KangRoger的 ec 读书笔记
https://blog.csdn.net/kangroger/category_2771821.html
[2]《Effective C++》:条款42 模板编程的隐式接口
https://blog.csdn.net/KangRoger/article/details/44182087