菱形继承与虚继承

Posted by CongYu on February 18, 2022

参考文献

https://www.fluentcpp.com/2017/09/12/how-to-return-a-smart-pointer-and-use-covariance/

继承类型:

  • 单继承
  • 多继承
  • 深继承
  • 菱形继承

1. 菱形继承

菱形继承(Diamond Inheritance)是多重继承中的一个经典问题,当一个类通过多条路径继承同一个基类时就会出现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Animal {
public:
    int age;
    virtual void speak() { std::cout << "Animal speaks\n"; }
    void eat() { std::cout << "Animal eats\n"; }
};

class Mammal : public Animal {
public:
    void breathe() { std::cout << "Mammal breathes\n"; }
};

class Bird : public Animal {
public:
    void fly() { std::cout << "Bird flies\n"; }
};

// 问题:Bat继承了两个Animal子对象
class Bat : public Mammal, public Bird {
public:
    void echolocate() { std::cout << "Bat uses echolocation\n"; }
};

菱形继承导致的问题:

  1. 数据成员重复
    1
    2
    3
    4
    5
    
    Bat bat;
    // bat对象中包含两个Animal子对象,因此有两个age成员
    // std::cout << bat.age;  // 错误!编译器不知道使用哪个age
    std::cout << bat.Mammal::age;  // 明确指定路径
    std::cout << bat.Bird::age;    // 明确指定路径
    
  2. 函数调用二义性
    1
    2
    3
    4
    
    Bat bat;
    // bat.speak();  // 错误!不知道调用哪个speak()
    bat.Mammal::speak();  // 通过Mammal路径调用
    bat.Bird::speak();    // 通过Bird路径调用
    
  3. 内存浪费
    1
    2
    
    std::cout << "Bat对象大小: " << sizeof(Bat) << " bytes\n";
    // 包含两个完整的Animal子对象,造成内存浪费
    

没有虚继承的内存布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Bat对象内存布局(普通继承):
+---------------------------+
|    Animal部分(来自Mammal)  |
|    - age                  |
|    - vptr                 |
+---------------------------+
|    Mammal特有部分          |
+---------------------------+
|    Animal部分(来自Bird)   |
|    - age (重复!)          |
|    - vptr                 |
+---------------------------+
|    Bird特有部分            |
+---------------------------+
|    Bat特有部分             |
+---------------------------+

2. 虚继承

2.1 概述

虚继承通过确保共同基类只有一个实例来解决菱形继承问题:

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
class Animal {
public:
    int age = 0;
    virtual void speak() { std::cout << "Animal speaks\n"; }
    void eat() { std::cout << "Animal eats\n"; }
    virtual ~Animal() = default;
};

class Mammal : virtual public Animal {  // 虚继承
public:
    void breathe() { std::cout << "Mammal breathes\n"; }
};

class Bird : virtual public Animal {    // 虚继承
public:
    void fly() { std::cout << "Bird flies\n"; }
};

class Bat : public Mammal, public Bird {
public:
    void echolocate() { std::cout << "Bat uses echolocation\n"; }
    
    // 现在可以直接访问Animal的成员
    void setAge(int a) { age = a; }  // 明确只有一个age
    void makeSound() { speak(); }    // 明确只有一个speak()
};

虚继承后的使用:

1
2
3
4
5
6
7
8
Bat bat;
bat.age = 5;        // 正确!只有一个age
bat.speak();        // 正确!只有一个speak()
bat.breathe();      // Mammal的方法
bat.fly();          // Bird的方法
bat.echolocate();   // Bat的方法

std::cout << "优化后Bat对象大小: " << sizeof(Bat) << " bytes\n";

2.2 实现原理

虚继承通过虚基类表(Virtual Base Table)实现,类似于虚函数表。

虚继承的内存布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Bat对象内存布局(虚继承):
+---------------------------+
|    Mammal部分              |
|    - vptr (vtable)        |
|    - vbptr (vbtable)      | <- 虚基类表指针
+---------------------------+
|    Bird部分                |
|    - vptr (vtable)        |
|    - vbptr (vbtable)      | <- 虚基类表指针
+---------------------------+
|    Bat特有部分             |
+---------------------------+
|    共享的Animal部分         | <- 只有一个实例
|    - age                  |
|    - vptr                 |
+---------------------------+

虚基类表(VBTable)示意:

1
2
3
4
5
6
7
8
9
Mammal的VBTable:
+------------------+
| Animal偏移量      |  <- 指向共享Animal子对象的偏移
+------------------+

Bird的VBTable:
+------------------+
| Animal偏移量      |  <- 指向同一个共享Animal子对象的偏移
+------------------+

2.3 虚继承的构造和析构

虚继承中,最派生类负责构造虚基类:

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
28
29
30
31
32
33
34
35
36
37
38
class Animal {
protected:
    int age;
public:
    Animal(int a) : age(a) {
        std::cout << "Animal构造: age=" << age << std::endl;
    }
    virtual ~Animal() {
        std::cout << "Animal析构" << std::endl;
    }
};

class Mammal : virtual public Animal {
public:
    Mammal(int a) : Animal(a) {  // 这个调用在Bat构造时被忽略
        std::cout << "Mammal构造" << std::endl;
    }
    ~Mammal() { std::cout << "Mammal析构" << std::endl; }
};

class Bird : virtual public Animal {
public:
    Bird(int a) : Animal(a) {    // 这个调用在Bat构造时被忽略
        std::cout << "Bird构造" << std::endl;
    }
    ~Bird() { std::cout << "Bird析构" << std::endl; }
};

class Bat : public Mammal, public Bird {
public:
    // 最派生类必须直接初始化虚基类
    Bat(int a) : Animal(a),      // 只有这个调用有效
                 Mammal(a),      // Mammal中的Animal(a)被忽略
                 Bird(a) {       // Bird中的Animal(a)被忽略
        std::cout << "Bat构造" << std::endl;
    }
    ~Bat() { std::cout << "Bat析构" << std::endl; }
};

构造顺序示例:

1
2
3
4
5
6
Bat bat(10);
// 输出:
// Animal构造: age=10    <- 虚基类最先构造
// Mammal构造           <- 第一个直接基类
// Bird构造             <- 第二个直接基类  
// Bat构造              <- 最派生类

析构顺序(与构造相反):

1
2
3
4
5
// bat对象销毁时的输出:
// Bat析构
// Bird析构
// Mammal析构
// Animal析构           <- 虚基类最后析构

2.3 性能考虑

内存开销:

1
2
3
4
5
6
7
8
class Normal { int data; };
class VirtualBase { int data; };
class NormalDerived : public Normal { int more_data; };
class VirtualDerived : virtual public VirtualBase { int more_data; };

std::cout << "普通继承大小: " << sizeof(NormalDerived) << " bytes\n";
std::cout << "虚继承大小: " << sizeof(VirtualDerived) << " bytes\n";
// 虚继承通常多占用一个指针大小的空间(vbptr)

访问开销:

1
2
3
4
5
6
7
8
// 普通继承:直接偏移访问
// obj.member  ->  *(obj_ptr + offset)

// 虚继承:需要通过vbtable间接访问
// obj.virtual_base_member  ->  
//   1. 读取vbptr
//   2. 查找vbtable获取偏移
//   3. *(obj_ptr + virtual_offset)

2.4 实际应用场景

标准库中的虚继承示例:

1
2
3
4
5
6
7
// std::iostream的实现使用了虚继承
class basic_ios { /* ... */ };
class basic_istream : virtual public basic_ios { /* ... */ };
class basic_ostream : virtual public basic_ios { /* ... */ };
class basic_iostream : public basic_istream, public basic_ostream {
    // 避免basic_ios的重复,使用虚继承
};

实际应用示例:

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
28
29
30
31
32
33
// 图形界面框架中的应用
class Drawable {
public:
    virtual void draw() = 0;
    void setVisible(bool visible) { visible_ = visible; }
protected:
    bool visible_ = true;
};

class Widget : virtual public Drawable {
public:
    void setSize(int w, int h) { width_ = w; height_ = h; }
protected:
    int width_ = 0, height_ = 0;
};

class Clickable : virtual public Drawable {
public:
    void onClick(std::function<void()> callback) { onClick_ = callback; }
protected:
    std::function<void()> onClick_;
};

// Button同时需要Widget和Clickable的功能
// 但不需要两个Drawable基类
class Button : public Widget, public Clickable {
public:
    void draw() override {
        if (visible_) {  // 只有一个visible_成员
            std::cout << "Drawing button " << width_ << "x" << height_ << std::endl;
        }
    }
};

2.5 最佳实践

何时使用虚继承:

  1. 确实存在菱形继承结构
  2. 需要共享基类的单一实例
  3. 逻辑上基类只应该有一个实例

何时避免虚继承:

  1. 性能敏感的代码
  2. 简单的继承层次
  3. 不确定是否需要时(遵循YAGNI原则)

最佳实践:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 虚基类应该尽可能简单
class Base {
public:
    virtual ~Base() = default;
protected:
    Base() = default;  // 保护构造函数,只允许派生类构造
};

// 2. 避免在虚基类中放置数据成员(除非必要)
class DataBase {
private:
    static int next_id_;
protected:
    int id_;
    DataBase() : id_(++next_id_) {}  // 每个实例都有唯一ID
public:
    int getId() const { return id_; }
    virtual ~DataBase() = default;
};

// 3. 明确文档说明虚继承的使用
class InterfaceA : virtual public Base {
    // 文档:使用虚继承以支持多重继承而不产生Base的重复实例
};

性能测试示例:

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
28
29
30
31
32
33
34
35
36
#include <chrono>

class VBase { public: int data = 42; };
class V1 : virtual public VBase {};
class V2 : virtual public VBase {};
class VDerived : public V1, public V2 {};

class NBase { public: int data = 42; };
class N1 : public NBase {};
class N2 : public NBase {};
class NDerived : public N1, public N2 {};

void performanceTest() {
    const int iterations = 10000000;
    
    VDerived vd;
    NDerived nd;
    
    auto start = std::chrono::high_resolution_clock::now();
    for(int i = 0; i < iterations; ++i) {
        volatile int x = vd.data;  // 虚继承访问
    }
    auto end = std::chrono::high_resolution_clock::now();
    std::cout << "虚继承访问时间: " << 
        std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() 
        << " 微秒\n";
    
    start = std::chrono::high_resolution_clock::now();
    for(int i = 0; i < iterations; ++i) {
        volatile int x = nd.N1::data;  // 普通继承访问
    }
    end = std::chrono::high_resolution_clock::now();
    std::cout << "普通继承访问时间: " << 
        std::chrono::duration_cast<std::chrono::microseconds>(end - start).count() 
        << " 微秒\n";
}

关键要点总结:

  1. 问题解决:虚继承解决菱形继承中的二义性和重复问题
  2. 实现原理:通过虚基类表实现,最派生类负责虚基类构造
  3. 性能代价:额外的指针和间接访问开销
  4. 使用场景:标准库iostream、GUI框架等需要避免重复基类的场景
  5. 设计原则:只在确实需要时使用,虚基类应该保持简单