44.处理模板化基类内的名称

问题实例

 
假定我们需要撰写一个程序,它需要传递信息至不同的公司。信息或者是明文,或者是密码。如果我们在编译期可以确定发送至哪家公司,就可以采用基于template的解决方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class CompanyA{
public:
...
void sendCleartext(const string& msg);
void sendEncrypted(const string& msg);
...
};
class CompanyB{
public:
...
void sendCleartext(const string& msg);
void sendEncrypted(const string& msg);
...
};
class MsgInfo {...};//保存信息
template<typenmae Company>
class MsgSender{
public:
...
void sendClear(const MsgInfo& info){
string msg;
//根据info产生信息
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info);
};

显然这种处理方案是合理的。
但如果我们希望在每次发送信息时完成记录,我们通过派生一个derived class来完成这种行为:
1
2
3
4
5
6
7
8
9
10
template<typenmae Company>
class LoggingMsgSender:public MsgSender<Company>{
public:
...
void sendClearMsg(const MsgInfo& info){//避免遮蔽名称
//log sth
sendClear(info);//试图调用base class函数,无法编译
//log sth
}
};

这段程序逻辑上无误,但编译器会拒绝编译,并给出sendClear不存在这样的错误信息。


问题剖析

 
报错原因在于,当编译器遭遇class template LoggingMsgSender定义式时,并不了解其基类,因为此时基类尚未被具现化。
再具体一些,我们假定有CompanyZ坚持使用加密通讯:

1
2
3
4
5
6
class CompanyZ{
public:
...//不提供sendClear
void sendEncrypted(const string& msg);
...
};

那一般化的MsgSender template对CompanyZ并不合适,因为该template提供了sendClear。因此我们针对CompanyZ做出了一个特化:
1
2
3
4
5
6
template<>//全特化
class MsgSender<CompanyZ>{
public:
...
void sendSecret(const MsgInfo& info);
};

在完成了全特化后,我们再次考虑derived class LoggingMsgSender:
1
2
3
4
5
6
7
8
9
10
template<typenmae Company>
class LoggingMsgSender:public MsgSender<Company>{
public:
...
void sendClearMsg(const MsgInfo& info){
//log sth
sendClear(info);//如果参数为CompanyZ,该函数不存在
//log sth
}
};

如今我们可以清楚地看出编译器拒绝调用的原因:base class template可能被特化,且特化版本可能不具备与一般性版本一致的接口。编译器拒绝在templated base classes内寻找继承而来的名称,面向对象的规则在泛型编程中依然不再适用。


解决方案

  1. 使用this->
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template<typenmae Company>
    class LoggingMsgSender:public MsgSender<Company>{
    public:
    ...
    void sendClearMsg(const MsgInfo& info){
    //log sth
    this->sendClear(info);//假设继承了sendClear,隐式接口
    //log sth
    }
    };
  2. 使用using
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    template<typenmae Company>
    class LoggingMsgSender:public MsgSender<Company>{
    public:
    using MsgSender<Company>::sendClear;//假设存在
    ...
    void sendClearMsg(const MsgInfo& info){
    //log sth
    sendClear(info);
    //log sth
    }
    };
    值得注意的是,这里使用using与Effective C++ 34中的作用并不相同,这里的情况并非发生了名称遮蔽,而是主动要求编译器前往base class内部查找对象函数。
  3. 使用作用域运算符明确指出该名称
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template<typenmae Company>
    class LoggingMsgSender:public MsgSender<Company>{
    public:
    ...
    void sendClearMsg(const MsgInfo& info){
    //log sth
    MsgSender<Company>::sendClear(info);
    //log sth
    }
    };
    第三种方法不建议使用,因为如果名称是一个virtual函数,这种做法关闭了virtual绑定行为。

解决方案剖析

 
上述的三种方法其实都是在做同一件事:对编译器承诺“base class template的任何特化版本都将支持其一般版本所提供的接口”。
面对“指涉base class mrmbers”的无效references,编译器的诊断可能在早期(当解析dc template的定义式时),也可能发生在晚期(当template被特定实参具现化时)。c++一般惯于较早诊断,这也就是为什么“当base classes从templates中被具现化时”,它假设它对base classes的内容一无所知的原因。