条款43:学习处理模板化基类内的名称
Know how to access names in templatizedbase classes
假设我们要写一个应用程序,它可以把消息传送到几个不同的公司去,消息既可以以加密方式也可以以明文(不加密)的方式传送。如果我们有足够的信息在编译期间确定哪个消息将要发送给哪个公司,我们就可以采用基于模板的解法:
class CompanyA {
public:
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
...
};
class CompanyB {
public:
void sendCleartext(const std::string& msg);
void sendEncrypted(const std::string& msg);
...
};
... // classes for other companies
class MsgInfo { ... }; // 这个类用来保存信息,以便将来产生信息
template<typename Company>
class MsgSender {
public:
... // 构造函数、析构函数,等等
void sendClear(const MsgInfo& info)
{
std::string msg;
在这儿,根据info产生信息
Company c;
c.sendCleartext(msg);
}
void sendSecret(const MsgInfo& info) { ... }
// 类似sendClear,唯一不同的是这里调用c.sendEncrypted
};
这个做法行得通,但是假设我们有时需要在每次发送消息的时候把一些信息记录到日志中。通过一个派生类可以很简单地增加这个功能,下面这个似乎是一个合理的方法:
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
void sendClearMsg(const MsgInfo& info)
{
将“传送前”信息写至log;
sendClear(info); // 调用基类函数,无法通过编译
将“传送后”信息写至log;
}
...
};
注意派生类中的 信息传送函数的名字 (sendClearMsg) 与它的基类中的那个(sendClear)不同。这是一个好的设计,因为它避免了遮掩基类中的名字和重新定义一个继承来的non-virtual函数。但是上面的代码不能通过编译,编译器会抱怨sendClear不存在。
问题在于当编译器遇到类模板LoggingMsgSender的定义时,它们不知道它从哪个类继承。当然它继承的是 MsgSender<Company>,但是 Company 是一个模板参数,这个直到更迟一些才能被确定(当 LoggingMsgSender 被实例化的时候)。不知道 Company 是什么,就没有办法知道类MsgSender<Company> 是什么样子的。特别是,没有办法知道它是否有一个 sendClear函数。
为了使问题具体化,假设我们有一个坚持加密通讯的类CompanyZ:
class CompanyZ {
public: // 这个类不提供sendCleartext函数
void sendEncrypted(const std::string& msg);
...
};
MsgSender模板不适用于 CompanyZ,因为那个模板提供一个sendClear函数,对于 CompanyZ对象没有意义。我们可以创建一个MsgSender针对CompanyZ 的特化版本:
template<> // 一个全特化的MsgSender,删除了sendClear
classMsgSender<CompanyZ> {
public:
...
void sendSecret(const MsgInfo&info){ ... }
};
这个类定义开始处的"template <>"语法,它表示这是一个用于模板参数为 CompanyZ 时的 MsgSender模板的特化版本。只要类型参数被定义成了 CompanyZ,再没有其他模板参数可供变化。
再次考虑派生类LoggingMsgSender:
sendClear(info); // if Company == CompanyZ,这个函数不存在
就像注释中写的,当基类是MsgSender<CompanyZ> 时,这里的代码是无意义的,因为那个类没有提供 sendClear函数。这就是为什么 C++ 拒绝这个调用:它认识到基类模板可能被特化,而这个特化不一定提供和通用模板相同的接口。因此它通常会拒绝在模板化基类(MsgSender<Company>)中寻找继承来的名称(SendClear)。在某种意义上,当我们从Object-oriented C++ 跨越到 Template C++时,继承就不像以前那样通行无阻了。
有三种方法可以解决此问题。
1. 在被调用的基类函数前面加上"this->":
this->sendClear(info);
2. 使用using声明式:
usingMsgSender<Company>::sendClear; // 告诉编译器,请它假设sendClear位于基类内
void sendClearMsg(const MsgInfo& info)
{
...
sendClear(info);
...
}
这里的情形不是基类名字被派生类名字隐藏,而是如果我们不告诉它去做,编译器就不会搜索 基类域。
3. 显式指定被调用的函数是在基类中的:
MsgSender<Company>::sendClear(info);
但这往往是最不让人满意的一个解法,因为如果被调用函数是 virtual,显式限定会关闭 virtual绑定行为。
从名字可见性的观点来看,这里每一个方法都做了同样的事情:它向编译器保证任何后继的基类模板的特化都将支持通用模板提供的接口。所有的编译器在解析一个像LoggingMsgSender 这样的派生类模板时,这样一种保证都是必要的,但是如果保证被证实不成立,真相将在后继的编译过程中暴露。例如,如果后面的源代码中包含这些,
LoggingMsgSender<CompanyZ>zMsgSender;
MsgInfomsgData;
... // 在msgData内放置信息
zMsgSender.sendClearMsg(msgData); // error! won't compile
对 sendClearMsg 的调用将不能编译,因为在此刻,编译器知道基类是模板特化MsgSender<CompanyZ>,它们也知道那个类没有提供sendClearMsg试图调用的 sendClear函数。
从根本上说,本条款讨论的是编译器是会早些(当派生类模板定义被解析的时候)诊断对基类成员的非法引用,而这就是为什么当那些类被模板实例化的时候,它假装不知道基类的内容。
· 在派生类模板中,可以经由"this->" 前缀,经由 using 声明式,或经由一个显式基类限定引用基类模板中的名称。
条款44:将与参数无关的代码抽离template(1)
Factor parameter-independent code out oftemplate
如果你不小心,使用模板可能导致代码膨胀:重复的(或几乎重复的)的代码数据,或两者都有的二进制码。结果会使源代码看上去紧凑而整洁,但是目标代码臃肿而松散,所以你需要了解如何避免这样的二进制扩张。
你的主要工具有一个有气势的名字:共性与变性分析,但其概念非常平民化。
当你写一个函数,你意识到这个函数的实现的某些部分和另一个函数的实现本质上是相同的,你会从这两个函数中分离出通用的代码,放到第三个函数中,并让那两个函数来调用这个新的函数。也就是说,你分析那两个函数以找出那些共性与变性的构件,你把共性的构件移入一个新的函数,并把变性的构件保留在原函数中。类似地,如果你写一个类,而且你意识到这个类的某些构件和另一个类是相同的,你把会通用构件移入一个新类中,然后使用继承或复合使得原来的类可以访问这些通用特性。
在写模板时,你要做同样的分析,而且用同样的方法避免重复。模板中的重复是隐式的:仅有一份模板源代码,所以你必须培养自己去判断在一个模板被实例化多次后可能发生的重复。例如,假设你要为固定大小的正方矩阵写一个模板,支持逆矩阵运算。
template<typenameT, std::size_t n> // 支持n×n矩阵,元素类型为T的对象
class SquareMatrix {
public:
...
void invert(); // 求逆矩阵
};
这个模板取得一个类型参数T,它还有一个非类型参数size_t。非类型参数比类型参数更不通用,但是它们是完全合法的,而且,就像在本例中,它们可以非常自然。现在考虑以下代码:
SquareMatrix<double,5> sm1;
sm1.invert(); // call SquareMatrix<double, 5>::invert
SquareMatrix<double,10> sm2;
sm2.invert(); // call SquareMatrix<double, 10>::invert
这里将有两个invert被实例化。这两个函数不是相同的,因为一个作用于 5 x 5 矩阵,而另一个作用于 10 x 10矩阵,但是除了常数 5 和 10 以外,这两个函数是相同的。这是一个发生模板导致的代码膨胀的经典方法。
我们的直觉是创建取得一个值作为参数的函数版本,然后用 5 或10 调用这个参数化的函数以代替复制代码。以下是对SquareMatrix的第一次修改:
template<typenameT> // 于尺寸无关的基类
class SquareMatrixBase {
protected:
...
void invert(std::size_t matrixSize);// 给定尺寸求逆矩阵
};
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
private:
usingSquareMatrixBase<T>::invert; // 避免遮掩基类里的invert
public:
...
void invert() { this->invert(n); } // inline调用基类版的invert
};
就像你能看到的,invert 的参数化版本是在基类SquareMatrixBase 中的。与 SquareMatrix 一样,SquareMatrixBase 是一个模板,但它参数化的仅仅是矩阵中的对象的类型,而没有矩阵的大小。
SquareMatrixBase::invert 仅仅是一个计划用于派生类以避免代码重复的方法,所以它是 protected而不是 public。调用它的额外成本应该为零,因为派生类的invert使用 inline函数调用基类版本(这个inline 是隐式的)。这些函数使用了 "this->" 标记,因为如果不这样,在模板化基类中的函数名(诸如SquareMatrixBase<T>)会被派生类隐藏。另外,SquareMatrix 和SquareMatrixBase 之间的继承关系是private 的。这准确地反映了基类存在的理由仅仅是简化派生类的实现的事实,而不是表示SquareMatrix和SquareMatrixBase之间的一个概念上的 is-a 关系。