原文出自:http://www.codeproject.com/Tips/1023429/The-Virtual-Inheritance-and-Funny-Tricks
简介
为解决钻石问题,C++ 引入了虚继承,改变了子类调用父类构造函数的方式。这种“副作用”对某些不同寻常的类实现非常有用。
虚继承
虚继承的引入主要是为了解决多继承环境下有歧义的层次组合问题(通常被称为“钻石问题”)。例如,FAQ Where in a hierarchy should I use virtual inheritance?。下面我们回忆一下一些有趣的细节。
考虑下面的类定义:
class Base { public: Base(int n) : value(n) { std::cout << "Base(" << n << ")"<< std::endl; // 输出传入值:Base(N) } Base() : value(0) { std::cout << "Base()"<< std::endl; // 没有传入值:Base() } ~Base() { std::cout << "~Base()"<< std::endl; } int value; }; class One : public Base { public: One() : Base(1) { std::cout << "One()"<< std::endl; } ~One() { std::cout << "~One()"<< std::endl; } }; class Two : public Base { public: Two() : Base(2) { std::cout << "Two()"<< std::endl; } ~Two() { std::cout << "~Two()"<< std::endl; } }; class Leaf : public One, public Two { public: Leaf() : { std::cout << "Leaf()"<< std::endl; } ~Leaf() { std::cout << "~Leaf()"<< std::endl; } };
在这个实现中,Leaf
实例持有两个Base
类的拷贝:第一个来自于One
,第二个来自于Two
。这样的实现使得下面语句:
Leaf lf; lf.value = 0; // 编译错误!
会出现歧义。我们必须显式指定,要么是lf.One::value = 0
,要么是lf.Two::value = 0
。
通常,我们会尽量避免一个Leaf
对象持有多个Base
类。这可以通过使用虚继承实现:我们可以在继承的子类添加virtual
关键字:
virtualclass One : public
Base…
以及
virtualclass Two : public
Base…
。使用virtual
关键字,Leaf
类仅会调用一次Base
类的构造函数,因而也就只构建了一个Base
。Leaf
内部只持有一个Base
子对象(该子对象也会被One
和Two
“共享”)。这就是我们所需要的。
那么问题来了,“编译器怎么知道该给Base
的构造函数传哪个参数?”的确,我们有两个选择:One
的构造函数调用了Base(1)
,Two
的构造函数调用了Base(2)
。那么,我们究竟该选哪个?答案很明显:哪个都不选。Leaf
的构造函数直接调用时,编译器选择的是Base()
的默认构造函数,而不是
1Base(
)
或者
2Base(
)
。因而,我们例子的输出将会是:
Base() // 注意:这里既不是 Base(1) 也不是 Base(2) One() Two() Leaf()
编译器会隐式在Leaf
初始化列表中添加Base()
的调用,并忽略其它Base()
构造函数的调用。因此,初始化列表类似这样:
class Leaf : public Base(), public One, public Two { ... }
当然,我们也可以给Leaf
的初始化列表显式添加Base(...)
语句,例如,我们需要给构造函数传入参数 3,那么就可以这么写:
class Leaf : public Base(3), public One, public Two { ... }
后面的例子的输出将会是:
Base(3) One() Two() Leaf()
这是非常重要的:Leaf
的构造函数直接调用了Base
的构造函数,而不是像非虚继承那样的间接调用(通过One
和Two
的构造函数。Leaf
的构造函数能够直接调用Base
的构造函数(绕过One
和Two
的构造函数)这一事实,在一些类系统设计上非常有用。
为了更全面认识,现在考虑几个有趣的例子。第一个,也就是著名的“最终类”问题。
最终类
所谓“最终类”,是一种能够在堆上或者栈上创建实例,但是不能被继承的类。换句话说,下面的代码是合法的:
Final fd; Final *pd = new Final();
但是,下面的代码则会给出一个编译错误:
class Derived : public Final{};
在 C++ 标准引入final
关键字之前的很长一段时间,最终类问题都是通过虚继承解决的。例如More C++ Idioms/Final Class这里所阐述的。其解决方案如下所示:
class Seal { friend class Final; Seal() {} }; class Final : public virtual Seal { public: Final() {} };
继承Final
会引发一个编译错误“不能访问Seal
类的私有成员”:
class Derived : public Final { public: Derived() {} // 编译错误: // cannot access private member of class Seal };
这个技巧的关键是虚继承:Derived
构造函数必须直接调用Seal
的构造函数,而不能通过Final
的构造函数间接调用。但是,这又是不允许的:Seal
类只有私有构造函数,而Derived
类又不是像Final
那样是Seal
的友元。Final
类本身允许调用Seal
的私有构造函数,因为它是Seal
的友元。这就是为什么Final
可以在栈上或者堆上创建对象。
这个解决方案建立在虚继承的基础之上。如果移除继承声明
virtualclass Final : public
Seal
中的virtual
关键字,Derived
类就可以通过Final
类间接调用Seal
的构造函数(因为后者是Seal
的友元),这个魔法就消失了。
第二个例子是另外一个著名的问题:在构造函数中调用虚函数。
在构造函数中调用虚函数
我们都知道,构造函数中不应该调用虚函数。例如:FAQ When my base class’s constructor calls a virtual function on its this object, why doesn’t my derived class’s override of that virtual function get invoked?这里,还有这里。
不能在构造函数中调用虚函数的原因在于,由于子类的覆盖还没有完成,因而虚函数的调用机制是被禁止的。在调用虚函数之前,我们必须等待构造函数完成了对象的创建。
幸运的是,有一个简单的方法:如果我们能够在函数调用之后立即运行另外一段代码。考虑下面的代码:
f(const caller_helper& caller = caller_helper()) { ... }
在调用函数f()
之前,编译器创建了一个临时对象caller_helper
,调用之后,这个临时对象被销毁。这意味着,析构函数~caller_helper()
在函数f()
完成之后立刻调用。这就是我们所需要的。
解决方案如下:
class base; class caller_helper { public: caller_helper() : m_p(nullptr) {} void init(base* p) const { m_p = p; } ~caller_helper(); private: mutable base* m_p; }; class base { public: base(const caller_helper& caller) { caller.init(this); // 存储指针 } virtual void to_override() {} // 空实现 }; class derived : public base { public: derived(const caller_helper& caller = caller_helper()) : base(caller) {} virtual void to_override() { std::cout << "derived"<< std::endl; } }; class derived_derived : public derived { public: derived_derived(const caller_helper& caller = caller_helper()) : derived(caller) {} virtual void to_override() { std::cout << "derived_derived"<< std::endl; } }; caller_helper::~caller_helper() { if (m_p) m_p->to_override(); }
base
的构造函数将this
指针注册到caller_helper
对象。
base(const caller_helper& caller) { caller.init(this); // 存储指针 m_p = p; }
析构函数~caller_helper()
调用虚函数:
caller_helper::~caller_helper() { if(m_p) m_p->to_override(); }
子类构造函数参数中的默认参数const caller_helper& caller = caller_helper()
输出为:
... derived_derived(const caller_helper& caller = caller_helper()) : derived(caller) {} ... derived(const caller_helper& caller = caller_helper()) : base(caller) {}
这就是我们需要的:析构函数~caller_helper()
在相应子类构造函数退出之后立即调用。
代码:
derived td; derived_derived tdd;
输出:
derived derived_derived
这个解决方案是可行的,但是有一个值得注意的缺陷。
的确,构造函数完成时,虚函数可以被调用,但是,我们必须在构造函数参数中加入一个默认参数const caller_helper& caller = caller_helper()
,并且我们必须为类层次结构中的每一个子类都添加这么一个参数,例如我们的derived()
和derived_derived()
。
忘记为derived
添加参数并不容易,因为其父类base
没有默认构造函数。但是,忘记为derived_derived
添加这个参数却很容易,但是它的父类derived
的确有默认构造函数。
这里,虚继承再一次拯救了世界。我们可以定义
virtualclass derived : public
base
,强制所有子类直接调用base(const caller_helper& caller)
构造函数。
最后经过修改,我们的代码如下:
class base; class caller_helper { public: caller_helper() : m_p(nullptr) {} void init(base* p) const { m_p = p; } ~caller_helper(); private: mutable base* m_p; }; class base { public: base(const caller_helper& caller) { caller.init(this); // 存储指针 } virtual void to_override(){} // 空实现 }; class derived : public virtual base { public: derived(const caller_helper& caller = caller_helper()) : base(caller) {} virtual void to_override() { std::cout << "derived"<< std::endl; } }; class derived_derived : public derived { public: derived_derived(const caller_helper& caller = caller_helper()) : base(caller) {} virtual void to_override() { std::cout << "derived_derived"<< std::endl; } }; caller_helper::~caller_helper() { if (m_p) // 检查指针是否初始化,如果没有初始化完成则不调用 m_p->to_override(); }
结论
总结一下前面提到的种种思路。
- 虚继承着眼于子类直接调用虚基类的构造函数,而不是像非虚继承那样通过构造函数链间接调用。
- 利用这一特性,结合私有成员访问控制,我们可以设计出最终类模式。当然,随着 C++11 标准引入
final
关键字,这一模式已经不像原来那样重要,但仍不失为一种好的模式。 - 如果一个函数有一个传值的类参数,那么,这个类的实例会在函数调用之前创建,在函数调用完毕之后销毁。利用这一特性,我们可以在类构造函数完成并且虚表也构造完成之后调用一个虚函数。这允许我们调用正确的虚函数。结合虚继承,我们可以实现一种“在构造函数中”调用虚函数的优雅模式。