More Effective C++之技术Techniques,Idioms,Patterns
本章描述C++程序员常常遭遇的一些问题的解决办法,这些解法都已获得证明。本书把这样的解法称为techniques(技术),也有称为idioms(惯用手法)或patterns(模式)。不论如何称呼它们,当每天与软件开发过程中的各种小冲突搏斗时,本章提供的信息可以带给我们很多帮助。
条款25:将constructor和non-member function虚化
constructor虚化
第一次面对“virtual constructors”时,似乎不觉得有什么道理可言。是的,当我们手上有一个对象的pointer或reference,而不着知道该对象的真正类型是什么的时候,我们会调用virtual function(虚函数)以完成“因类型而异的行为”。当我们尚未获得对象,但已经确知需要什么类型的时候,我们会调用constructor以构造对象。那么,什么是virtual constructor呢?
很简单,虽然virtual constructors似乎有点荒谬,但它们很有用。假设我们写了一个软件,用来处理时事新闻,其内容由文字和图形构成。我们可以把程序组织成这样:
class NLComponent { // 抽象基类,用于时事消息的组件,其中内含一个纯虚函数
public:
...
};
class TextBlock : public NLComponet {
public:
... // 没有内含纯虚函数
};
class Graphic: public NLComponent {
public:
... // 没有内含纯虚函数
};
class NewsLetter { // 一份时事通信是由一系列的NLComponent对象构成的。
public:
...
private:
list<NLComponent*> componets;
};
这些class彼此间的关系如下:
NewsLetter所使用的list class由Standard Template Library提供,后者是C++标准程序库的一部分。list对象的行为就像双向链表(double linked lists)——尽管它们不一定得用双向链表实现。
NewLetter对象尚未开始运作的时候,可能存储于磁盘中。为了能够根据磁盘上的数据产出一份Newsletter,如果我们让NewsLetter拥有一个constructor并用istream作为自变量,会很方便。这个constructor将从stream读取数据以便产生必要的核心数据结构:
class NewsLetter {
public:
NewsLetter(istream& str);
...
};
此constructor的伪代码(pseudo code)可能看起来像这样:
NewsLetter::NewsLetter(istream& str) {
while(str) {
read the next component object from str;
add the object to the list of this
newsletter's components;
}
}
或者,如果将棘手的东西搬移到另一个名为readComponent的函数,就变成这样:
class NewsLetter {
public:
...
private:
// 从str读取下一个NLComponent的数据,
// 产生组件(component),并返回一个指针指向它。
static NLComponent* readComponent(istream& str);
...
};
NewsLetter::NewsLetter(istream& str) {
while(str) {
components.push_back(readComponent(str));
}
}
思考一下,readComponent做了一些什么事。它产生一个崭新对象,或许是TextBlock,或许是个Graphic,视读入的数据而定。由于它产生新对象,所以行为仿若constructor,但它能够产生不同类型的对象,所以我们称它为virtual constructor。所谓virtual constructor视其获得的输入,可产生不同类型的对象。Virtual constructors在许多情况下有用,其中之一就是从磁盘(或网络或磁带等)读取对象信息。
有一种特别的virtual constructor——所谓virtual copy constructor——也被广泛地运用。Virtual copy constructor会返回一个指针,指向其调用者(某对象)的一个新副本。基于这种行为,virtual copy constructors通常以copySelf或cloneSelf命名,或者像下面一样命名为clone。很少有其他函数能够比这个函数有更直接易懂的实现方式了:
class NLComponent {
public:
// 声明virtual copy constructor
virtual NLComponent * clone() const = 0;
...
};
class TextBlock : public NLComponent {
public:
virtual TextBlock* clone() const
{ return new TextBlock(*this); }
};
class Graphic : public NLComponent {
public:
virtual Graphic* clone() const
{ return new Graphic(*this); }
};
正如所见,class的virtual copy constructor就只是调用真正的copy constructor而已。“copy”这层意义对这两个函数而言是一样的。如果真正的copy constructor执行的是浅复制(shallow copy),virtual copy constructor也一样。如果真正的copy constructor执行的是深复制(deep copy),virtual copy constructor也一样。如果真正的copy constructor做了某些煞费苦心的动作,如reference-counting(引用技术)或copy-on-write(“写入时才复制”),virtual copy constructor也一样。因为时函数调用,保持了其行为的一致性。
注意上述实现手法乃利用“虚函数之返回类型”规则中的一个宽松点,那是晚些才被接纳的一个规则。当derived class重新定义其base class的一个虚函数时,不再需要一定得声明与原本相同的返回类型。如果函数的返回类型是个指针(或reference),指向一个base class,那么derived class的函数可以返回一个指针(或reference),指向该base reference的一个derived class。这并不会造成C++的类型系统门户洞开,却可准确声明出像virtual copy constructor这样的函数。这也就是为什么即使NLComponent的clone函数的返回类型是NLComponent*,TextBlock的clone函数却可以返回TextBlock*,而Graphic的clone函数可以返回Graphic*的原因。
NLComponent拥有一个virtual copy constructor,于是我们现在可以为NewsLetter轻松实现一个(正常的)copy constructor:
class NewsLetter {
public:
NewsLetter(const NewsLetter& rhs);
...
private:
list<NLComponent*> components;
...
};
NewsLetter::NewsLetter(const NewsLetter& rhs) {
for (auto it = rhs.components.begin(); it != rhs.components.end(); ++it) {
components.push_back(it->clone());
}
}
上面这段代码,其观念很简单:只要遍历即将被复制的那个NewsLetter对象的component list,并针对其中的每一个组件(component)调用其virtual copy constructor即可。在这里,我们需要一个virtual copy constructor,因为这个component list内含NLComponent对象指针,但我们知道每个指针真正指向的是一个TextBlock或是Graphic。我们希望无论指针真正指向什么,我们都可以复制它,virtual copy constructor可以达到这个目标。
将Non-Member Functions的行为虚化
就像constructors无法真正被虚化一样,non-member functions也是。然后就像我们认为应该能够以某个函数构造出不同类型的新对象一样,我们也认为应该可以让non-member-functions的行为视其参数的动态类型而不同。举个例子,假设我们希望为TextBlock和Graphic实现出ouput操作符,显而易见的一个做法是让ouput操作符虚化。然而output操作符(operator<<)获得一个ostream&作为左端自变量,因此它不可能成为TextBlock或Graphic classes的一个memeber function。尝试定义如下:
class NLComponent {
public:
// output operator的非传统声明
virtual ostream& operator <<(ostream& str) const = 0;
...
};
class TextBlock : public NLComponent {
public:
// output operator(也是打破传统)。
virtual ostream& operator <<(ostream& str) const ;
};
class Graphic : public NLComponent {
public:
// output operator(也是打破传统)。
virtual ostream& operator <<(ostream& str) const ;
};
TextBlock t;
Graphic g;
...
t << cout; // 通过operator << ,在cout身上打印t,注意cout不是在前面
g << cout; // 通过operator << ,在cout身上打印g,注意cout不是在前面
Clients必须把stream对象放在"<<"符号的右端,而那和传统的output操作符习惯不符。如果要回到正常的语法形式,我们必须将operator<<从TextBlock和Graphic classes身上移除,但如果那么做,我们就不再能够将它声明为virtual。
另一种做法是声明一个虚函数(例如,print)作为打印之用,并在TextBlock和Graphic中定义它。但如果我们这么做,TextBlock和Graphic对象的打印语法就和其他类型的打印语法不一致,因为其他类型都依赖operator<<作为输出之用。
这些解法没有一个令人满意。我们真正需要的是一个名为operator<<的non-memeber function,展现出类似print虚函数一般的行为。这一段“需求描述”其实已经非常接近其“做法描述”,是的,让我们同时定义operator<<和print,并令前者调用后者:
class NLComponent {
public:
virtual ostream& print(ostream& str) const = 0;
...
};
class TextBlock : public NLComponent {
public:
virtual ostream& print(ostream& str) const ;
};
class Graphic : public NLComponent {
public:
virtual ostream& print(ostream& str) const ;
};
inline ostream& operator<<(ostream &s, const NLComponent& c)
{ return c.print(s); }
显然,non-member functions的虚化十分容易:写一个虚函数做实际工作,再写一个什么都不做的非虚函数,只负责调用虚函数。当然啦,为了避免此巧妙安排蒙受函数调用所带来的成本,将非虚函数inline化。
学习心得
本主题介绍了构造函数和非成员函数的“虚化”技术。构造函数虚化分为两部分:一是类似于工厂函数,使用静态函数构造产生对象,二是利用clone虚函数,内部调用复制构造函数,完成虚拟拷贝构造的嫁接(因为事实上调用clone时并不需要当前类的名字来显示调用复制构造函数)。非成员函数的虚化,主要是在例如输出操作符<<不适合定义为成员函数时,也可以通过内部定义一个虚函数,然后在非成员函数中进行一次中转(调用类的虚函数),看上去将非成员函数也进行了虚化一般。此两种方法,不失为我们在编码过程中的有效参考。