在阅读ROS的源码的时候,遇到了这样的一个地方:
cpp
/** \brief %Controller with a specific hardware interface
*
* \tparam T The hardware interface type used by this controller. This enforces
* semantic compatibility between the controller and the hardware it's meant to
* control.
*/
template <class T>
class Controller: public virtual ControllerBase
{
public:
这个里面继承类有一个virtual
关键字,这个virtual
关键字的作用是什么呢,什么时候会需要这样的一个virtual
关键字呢,本篇文章将回答这些问题:
背景
在 ROS 控制框架(尤其是 ros_control
)中,控制器类通常会继承多个基类,这些基类可能最终都继承自同一个公共基类(例如 ControllerBase
)。这就会遇到菱形继承(diamond inheritance) 的问题。
普通继承下的问题
假设有以下继承关系:
ControllerBase
^ ^
| |
ControllerA ControllerB
\ /
\ /
MyController
如果 ControllerA
和 ControllerB
都是普通继承 ControllerBase
:
cpp
class ControllerA : public ControllerBase { ... };
class ControllerB : public ControllerBase { ... };
class MyController : public ControllerA, public ControllerB { ... };
那么 MyController
内部会有两份 ControllerBase
子对象,这会带来:
- 二义性访问:
MyController
中访问ControllerBase
成员时编译器会报错(不知道选哪一份)。 - 多份数据冗余:
ControllerBase
的状态被复制两份,很可能导致逻辑混乱。
虚拟继承(virtual
)的作用
虚拟继承的目的是在多重继承场景下,让所有派生类共享一份公共基类实例,避免菱形继承中的重复子对象。
如果这样改:
cpp
class ControllerA : public virtual ControllerBase { ... };
class ControllerB : public virtual ControllerBase { ... };
class MyController : public ControllerA, public ControllerB { ... };
那么不管你通过 ControllerA
还是通过 ControllerB
去访问 ControllerBase
,都会访问到同一份实例,不会重复创建。
回到你的例子
ros_control
框架中,Controller
模板类是所有具体控制器的一个中间基类,它通常可能和其他基类(比如插件接口、硬件接口适配器等)一起被多重继承,并且这些基类的共同祖先是 ControllerBase
。
如果 Controller
不使用 virtual
继承 ControllerBase
,一旦你有另外一个基类也继承了 ControllerBase
,那么最终你的实际控制器类就会陷入菱形继承问题。
因此,设计者直接在 Controller
这里用 public virtual ControllerBase
来预防将来在多重继承中出现菱形继承的问题。
总结
virtual
在这里的作用是:
- 解决/预防菱形继承 导致的重复基类子对象问题。
- 保证唯一性 :所有继承链里只保留一份
ControllerBase
成员。 - 提高兼容性与健壮性:即使未来添加新的中间类,也不会造成二义性访问。
✅ 一句话总结
因为 ros_control
框架的控制器体系存在多重继承的可能性,Controller
类用 virtual
继承 ControllerBase
是为了让最终派生类只保留一份 ControllerBase
实例,避免菱形继承带来的二义性与数据重复。
那为什么不所有的继承都使用虚拟继承呢?
这个问题很关键,很多人第一次学 virtual inheritance 的时候也会想------既然解决了菱形继承的问题,那干脆所有继承都写成虚拟继承是不是更安全?
答案是:不应该所有继承都用虚拟继承,原因主要有四点。
1. 虚拟继承有额外开销(复杂度 + 性能)
- 普通继承时,基类子对象位置在编译期固定在派生类对象的内存布局中,访问基类成员只需要固定偏移量。
- 虚拟继承为了支持"唯一基类",编译器会引入 虚基表(vbase table) 来在运行时计算基类子对象真实位置。
- 结果:
- 内存布局更复杂,访问虚基类成员速度可能略慢(需要多一次指针间接跳转)。
- 对象大小一般会增加(多一个指向虚基表的指针)。
2. 虚拟继承降低可读性和直观性
- 普通继承:构造函数按照继承链自动初始化基类对象。
- 虚拟继承:
- 最终派生类 必须负责虚基类的初始化,即使中间基类也定义了初始化逻辑,这会让初始化代码更分散。
- 容易让新手迷惑"这个基类的构造为什么不能在中间类里调用生效?"
例如:
cpp
struct A {
A(int x) { ... }
};
struct B : virtual A {
B() : A(1) {} // 这里并不会真正构造 A
};
struct C : virtual A {};
struct D : B, C {
D() : A(42) {} // 最终派生类必须管理 A 的构造
};
在
B
中初始化A(1)
是无效的,因为虚基类初始化由D
决定。
3. 虚拟继承不必要时反而制造麻烦
- 如果类层次很简单,没有菱形继承或公共基类重复问题,虚拟继承不会带来好处,反而增加了内存间接访问、初始化复杂度。
- 而且虚拟继承使得类设计绑定得更紧,对编译器和链接器都增加额外工作量。
4. 虚拟继承是为了特殊场景而设计的
- 它的主要用途就是解决多重继承中公共基类的重复 ,比如:
- 接口类(多重继承常见)
- 框架基类(如
ControllerBase
)
- 在常规一条继承链中(单继承),虚拟继承是完全没有意义的。
在 ROS ros_control
里的设计哲学
ControllerBase
是公共框架基类,有可能在多重继承中被重复继承 → 用虚拟继承。- 其他不需要多重继承的类就用普通继承,保持简单、高效。
- 这样只在必要的地方引入虚拟继承,避免全局复杂化。
✅ 一句话总结
虚拟继承是为了解决菱形继承的特殊工具,不是通用继承方式。它带来额外的运行和设计复杂度,因此只有在可能出现重复基类的情况下才使用,比如框架核心接口类。