C++ EBO 与空基类优化
更新时间:2026-08-27。本文是
languages/cpp/主题专家层第 3 篇。上一篇讲对象布局,这一篇抠一个细节:空类对象占多少内存? 答案是"至少 1 字节"——但作为基类被继承时,它可以占 0 字节。这背后是 C++ 的一个经典优化:空基类优化(EBO,Empty Base Optimization),std::tuple等标准库组件靠它省下大量内存。
本文要回答的问题
- 空类
struct E{}的sizeof为什么是 1?0 不行吗? - EBO 是什么?空基类为什么能"免费"叠加?
std::tuple<int, Empty, Empty>为什么不会比tuple<int>大多少?- 什么情况下 EBO 失效?多态空类、多继承空类怎么办?
空类为什么占 1 字节
struct Empty {};
static_assert(sizeof(Empty) == 1); // 不是 0!为什么不能是 0? 因为 C++ 要求"同一类型的两个不同对象地址必须不同"。如果 sizeof 是 0,那么 Empty a, b; 两个对象可能拿到同一个地址,无法区分。所以空类的最小尺寸是 1 字节(占一个独一无二的地址)。

EBO:基类可以"免费"
struct Empty {};
struct Derived : Empty {
int v;
};
static_assert(sizeof(Derived) == 4); // EBO 生效:基类占 0 字节作为基类的空类,编译器可以把它的大小压缩到 0——因为派生类对象内部,基类子对象不需要独立地址(访问它通过派生类对象地址即可)。这就是 EBO(Empty Base Optimization)。
对比:作为成员的空类没有这个待遇:
struct WithMember {
int v;
Empty e; // 成员
};
static_assert(sizeof(WithMember) == 8); // e 占 1 字节 + 对齐填充到 8| 位置 | 空类占多大 | 原因 |
|---|---|---|
| 独立对象 | 1 字节 | 需要唯一地址 |
| 基类 | 0 字节(EBO) | 不需要独立地址 |
| 成员 | 1 字节 + 对齐 | 成员必须有地址,且要参与对齐 |
std::tuple 的 EBO 应用
EBO 最大的价值体现在 std::tuple:它的实现用递归继承存储元素(见 变参模板),每个元素类型做一次继承——空类型作为基类就被优化掉了。
#include <tuple>
#include <type_traits>
struct EmptyA {};
struct EmptyB {};
std::tuple<int, EmptyA, EmptyB> t; // 两个空类型不占空间
static_assert(sizeof(t) == sizeof(int)); // 只占一个 int 的大小如果不用 EBO(把空类型当成员存),这个 tuple 会变成 int + 1 + 1 + 对齐填充 = 8 字节。EBO 让 tuple 的体积只由"非空成员"决定。
// 手写对比:非 EBO 的 naive 实现
template <typename... Ts> struct NaiveTuple {
Ts... values; // 示意:空类型各占 1 字节
}; // 大得多标准库的 EBO 依赖:std::function 等
标准库大量依赖 EBO 来消灭"空分配器/空比较器"的开销:
#include <functional>
// 无捕获 lambda 转成 std::function 后不额外占空间
std::function<void()> f = []() {};std::function 内部存储可调用对象时,如果可调用对象是空类(无捕获 lambda),EBO 让它不占额外存储。同理 std::vector<T, Alloc> 的空分配器、std::shared_ptr 的控制块,都靠 EBO 压缩体积。
EBO 失效的场景
EBO 不是永远生效,主要失效场景:
| 场景 | 失效原因 | 处理 |
|---|---|---|
| 空基类是多态类(有虚函数) | vptr 需要存储 | 无法优化 |
| 多个空基类共享同一偏移 | 需要区分地址 | 编译器扩展(MSVC 支持) |
| 空基类有非平凡的构造/析构 | 需要存储某些状态 | 视实现 |
| 作为成员而非基类 | 成员需独立地址 | 用 [[no_unique_address]] |
5.1 多态空类不适用 EBO
struct PolyEmpty {
virtual void f() {} // 有 vptr
};
struct D : PolyEmpty {
int v;
};
static_assert(sizeof(D) >= 8); // vptr(8) + int(4) + 填充EBO 只对"真·空"(无成员、无虚函数、无虚基类)的类生效——有 vptr 就不是空的。
5.2 C++20 [[no_unique_address]]
C++20 提供属性,让成员位置的"空类"也能共享存储:
struct Empty {};
struct WithAttr {
int v;
[[no_unique_address]] Empty e; // e 可与 v 共享地址(允许)
};
static_assert(sizeof(WithAttr) == 4); // C++20:EBO 效果延伸到成员[[no_unique_address]] 是 EBO 的"成员版":它允许空成员与其他成员共享地址,把 EBO 的收益带到成员位置。注意它是"允许"而非"强制"——空成员若与其他成员地址冲突(如类型相同),仍可能占位。
与本站性能主线衔接
- 对象布局:EBO 是对象布局的精妙一角,见 对象内存布局。
- 内存占用:空对象/空分配器不占空间,降低容器内存开销,衔接 内存布局。
- 虚表关联:多态空类带 vptr 无法 EBO,见 虚表与多态。
- 编译期零成本:EBO 是编译器静态优化的代表,与 编译器选项 的优化精神一致。
一句话总结
空类独立对象占 1 字节(需唯一地址),作为基类时 EBO 压缩到 0 字节,std::tuple、std::function 等标准库组件借此消灭空分配器/空状态的存储;EBO 对多态空类失效,C++20 的 [[no_unique_address]] 把优化延伸到成员位置。
上一篇:对象内存布局与编译期优化 下一篇:C++ ABI 与 name mangling