条款41:了解隐式接口和编译期多态
1、模板的编译期多态
例子:
#include<iostream>
using namespace std;
class ClassA{
public:
ClassA(size_t s) :size(s){}
size_t getSize() const{ return size; }
private:
size_t size;
};
class ClassB{
public:
ClassB(size_t s) :size(s){}
size_t getSize() const{ return size; }
private:
size_t size;
};
template<typename T>
bool doProcessing(T& t){
return t.getSize() > 10;
}
int main(){
ClassA classA(20);
ClassB classB(5);
cout << boolalpha << doProcessing(classA) << endl; //true,调用的是ClassA的getSize函数
cout << boolalpha << doProcessing(classB) << endl; //false,调用的是ClassB的getSize函数
system("pause");
return 0;
}
上述例子中doProcessing是个模板函数,涉及t的任何函数调用,如t.getSize()都有可能造成模板具现化,使这些调用得以成功。这样的具现行为发生在编译期。“以不同的模板参数具现化函数模板”会导致调用不同的函数,这就是所谓的编译器多态。
上述例子还说明了隐式接口。模板函数中的t.getSize() 就是隐式接口。隐式接口由一组有效表达式组成。这个例子中T的隐式接口必须提供一个名为getSize的成员函数,并且返回一个对象可以和int 型的10比较大小。
2、运行期多态
例子:
#include<iostream>
using namespace std;
class ClassA{
public:
ClassA(size_t s) :size(s){}
virtual size_t getSize() const{ return size; }
private:
size_t size;
};
class DerivedA :public ClassA{
public:
DerivedA(size_t s):ClassA(s){}
size_t getSize() const{ return ClassA::getSize() * 2; }
};
int main(){
ClassA classA(10);
DerivedA derivedA(10);
ClassA* pa = &classA;
cout << pa->getSize() << endl; //10,调用的是ClassA的getSize函数
pa = &derivedA;
cout << pa->getSize() << endl; //20,调用的是derivedA的getSize函数
system("pause");
return 0;
}
运行期多态是由virtual成员函数实现,上述例子中在运行期根据pa的动态类型决定究竟调用哪一个函数。上述例子说明了显式接口。如ClassA的getSize函数,显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。
请记住:
- class和template都支持接口和多态。
- 对classes而言,接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生在运行期。
- 对template参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生在编译期。
条款42:了解typename的双重意义
1、在模板声明式中,class和typename没有不同
例子:
template<class T> class Widget;
template<typename T> class Widget;
2、例子:有个模板函数,接受STL容器为参数,并打印出容器内的第二个元素。
#include<vector>
#include<iostream>
using namespace std;
template<typename T>
void print2nd(const T& container){
if (container.size() >= 2){
typename T::const_iterator iter(container.begin());
++iter;
cout << *iter << endl;
}
}
int main(){
vector<int> vec;
for (int i = 0; i < 5; ++i)
vec.push_back(i);
print2nd(vec);
system("pause");
return 0;
}
template内出现的名称如果相依于某个template参数,称之为从属名称。如果从属名称在class内呈前套状,称它为嵌套从属名称。上述例子中T::const_iterator就是嵌套从属名称。在我们知道T是什么之前,没办法知道T::const_iterator是否为一个类型。C++有个规则解析这一歧义状态:如果解析器在template中遭遇一个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。
任何时候再template中指涉一个嵌套从属类型名称时,必须在紧邻它的前一个位置放上关键字typename。上述例子中T::const_iterator前不加typename虽然在VS 2013也能正确运行,但最好加上。
3、“typename必须作为嵌套从属类型名称的前缀词”这一规则的例外是,typename不可以出现在base class list内的嵌套从属类型名称之前,也不可以在member initialization list(成员初值列)中作为baseclass修饰符。
例子:
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
}
……
};
请记住:
- 声明template参数时,前缀关键字class和typename可互换。
- 请使用关键字typename标识嵌套从属类型名称;但不得在base class lists(基类列)或member initialization list(成员初值列)内以它base class修饰符。
条款43:学习处理模板化基类的名称
例子:
#include<string>
#include<iostream>
using namespace std;
class Company{
public:
void sendClearText(const string& msg){
cout << msg << endl;
}
void sendEncrypted(const string& msg){
string str = msg;
for (int i = 0; i < str.length(); ++i)
str[i] = msg[i] - 'a'+'0';
cout << str << endl;
}
};
template<typename Company>
class MsgSender{
public:
void sendClear(const string& msg){
Company c;
c.sendClearText(msg);
}
void sendSecret(const string& msg){
Company c;
c.sendEncrypted(msg);
}
};
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>{
public:
void sendClearMsg(const string& msg){
sendClear(msg);
}
void sendSecretMsg(const string& msg){
sendSecret(msg);
}
};
int main(){
LoggingMsgSender<Company> log;
log.sendClearMsg("abc");
log.sendSecretMsg("abc");
system("pause");
return 0;
}
上述例子中按照作者的意思LoggingMsgSender类中sendClearMsg函数调用基类的sendClear函数和sendSecretMsg函数调用基类的sendSecret函数,都会导致编译通不过。因为当编译器遇见LoggingMsgSender时,并不知道它继承什么样的类,因为其中的Company是个template参数,不到LoggingMsgSender被具现化无法知道它是什么,也就不知道它是否有个sendClear函数或sendSecret函数。但我在VS 2013中能编译通过并正确运行。
有三个办法可以解决上述问题:
a、在基类函数调用动作之前加上this->,即:
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>{
public:
void sendClearMsg(const string& msg){
this->sendClear(msg);
}
void sendSecretMsg(const string& msg){
this->sendSecret(msg);
}
};
b、使用using声明式,即:
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>{
public:
using MsgSender<Company>::sendClear;
using MsgSender<Company>::sendSecret;
void sendClearMsg(const string& msg){
sendClear(msg);
}
void sendSecretMsg(const string& msg){
sendSecret(msg);
}
};
c、明白指出被调用的函数位于基类内,即:
template<typename Company>
class LoggingMsgSender : public MsgSender<Company>{
public:
void sendClearMsg(const string& msg){
MsgSender<Company>::sendClear(msg);
}
void sendSecretMsg(const string& msg){
MsgSender<Company>::sendSecret(msg);
}
};
这种放法有明显的缺陷:上述的明确资格修饰会关闭”virtual绑定行为“。
从名称可视点的角度,上述每一个解法做的事情都相同:对编译器承诺基类模板的任何特化版本都将支持其泛化版本所提供的接口。
请记住:
- 可在derived class templates内通过“this->”指涉base class templates内的成员名称,或藉由一个明白写出“base class资格修饰符”完成。