条款25:将constructor和non-member functions虚化(2)
如你所见,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("写入时才复制",见条款29),virtual copy constructor 也一样。啊,一贯性,多么奇妙的事情。
注意上述实现手法乃利用"虚函数之返回类型"规则中的一个宽松点,那是晚些才被接纳的一个规则。当 derived class 重新定义其 base class 的一个虚函数时,不再需要一定得声明与原本相同的返回类型。如果函数的返回类型是个指针(或 reference),指向一个 base class,那么 derived class 的函数可以返回一个指针(或 reference),指向该 base class 的一个 derived class。这并不会造成 C++(www.cppentry.com) 的类型系统门户洞开,却可准确声明出像 virtual copy constructors 这样的函数。这也就是为什么即使 NLComponent 的 clone 函数的返回类型是 NLComponent*,TextBlock 的 clone 函数却可以返回 TextBlock*,而 Graphic 的 clone 函数可以返回 Graphic* 的原因。
NLComponent拥有一个 virtual copy constructor,于是我们现在可以为 NewsLetter 轻松实现一个(正常的)copy constructor:
- class NewsLetter {
- public:
- NewsLetter(const NewsLetter& rhs); // copy constructor
- ...
- private:
- list<NLComponent*> components;
- };
-
- NewsLetter::NewsLetter(const NewsLetter& rhs)
- {
- // 迭代遍历 rhs 的 list,运用每个元素的 virtual copy constructor,
- // 将元素复制到此对象的 components list 中。以下代码的细节讨论,
- // 请见条款35。
- for (list<NLComponent*>::const_iterator it =
- rhs.components.begin();
- it != rhs.components.end();
- ++it) {
-
- // "it"指向 rhs.components 的目前元素,
- // 所以调用该元素的 clone 函数取得该元素的一个副本,
- // 然后将该副本加到本对象的 components list 尾端。
- components.push_back((*it)->clone());
- }
- }
我知道,除非你熟悉 Standard Template Library,否则上面这段代码看起来很诡异。但是其观念很简单:只要遍历即将被复制的那个 NewsLetter 对象的components list,并针对其中的每一个组件(component)调用其 virtual copy constructor 即可。在这里,我们需要一个 virtual copy constructor,因为这个components list 内含NLComponent 对象指针,但我们知道每个指针真正指向的是一个 TextBlock 或是 Graphic。我们希望无论指针真正指向什么,我们都可以复制它,virtual copy constructor 可以达到这个目标。
将 Non-Member Functions 的行为虚化
就像 constructors 无法真正被虚化一样,non-member functions(见条款E19)也是。然而就像我们认为应该能够以某个函数构造出不同类型的新对象一样,我们也认为应该可以让 non-member functions 的行为视其参数的动态类型而不同。举个例子,假设你希望为 TextBlock 和 Graphic 实现出 output 操作符,明显的一个办法就是让 output 操作符虚化。然而,output 操作符(operator<<)获得一个 ostream& 作为其左端自变量,因此它不可能成为 TextBlock 或 Graphic classes 的一个 member function。
(是可以啦,但是看看会发生什么事:
- class NLComponent {
- public:
- // output operator 的非传统声明。
- virtual ostream& operator<<(ostream& str) const = 0;
- ...
- };
-
- class TextBlock: public NLComponent {
- public:
- // virtual output operator(也是打破传统)。
- virtual ostream& operator<<(ostream& str) const;
- };
-
- class Graphic: public NLComponent {
- public:
- // virtual output operator(也是打破传统)。
- virtual ostream& operator<<(ostream& str) const;
- };
-
- TextBlock t;
- Graphic g;
- ...
- t << cout; // 通过 virtual operator<<,在 cout 身上
- // 打印出 t。注意此语法与传统不符。
-
- g << cout; // 通过 virtual operator<< 在 cout 身上
- // 打印出 g。注意此语法与传统不符。
Clients 必须把stream对象放在"<<"符号的右手端,而那和传统的output操作符习惯不符。如果要回到正常的语法形式,我们必须将 operator<< 从TextBlock 和Graphic classes身上移除,但如果这么做,我们就不再能够将它声明为virtual。)另一种做法是声明一个虚函数(例如,print)作为打印之用,并在 TextBlock 和 Graphic 中定义它。但如果我们这么做,TextBlock 和 Graphic 对象的打印语法就和其他类型对象的打印语法不一致,因为其他类型都依赖 operator<< 作为输出之用。
这些解法没有一个令人满意。我们真正需要的是一个名为operator<< 的non-member function,展现出类似print 虚函数一般的行为。这一段"需求描述"其实已经非常接近其"做法描述",是的,让我们同时定义 operator<< 和 print,并令前者调用后者:
- class NLComponent {
- public:
- virtual ostream& print(ostream& s) const = 0;
- ...
- };
-
- class TextBlock: public NLComponent {
- public:
- virtual ostream& print(ostream& s) const;
- ...
- };
-
- class Graphic: public NLComponent {
- public:
- virtual ostream& print(ostream& s) const;
- ...
- };
-
- inline
- ostream& operator<<(ostream& s, const NLComponent& c)
- {
- return c.print(s);
- }
显然,non-member functions 的虚化十分容易:写一个虚函数做实际工作,再写一个什么都不做的非虚函数,只负责调用虚函数。当然啦,为了避免此巧妙安排蒙受函数调用所带来的成本,你可以将非虚函数 inline 化(见条款E33)。
现在你知道如何让 non-member functions 视其某个自变量而虚化了。你可能会想,有没有可能让它们根据一个以上的自变量而虚化?可以,但是不容易。有多困难呢?请翻到条款31,那个条款专门讨论这个问题。