空基类优化(EBO):C++ 的瘦身技巧¶
有一种低调而高效的内存优化,总在你看不到的地方帮你省下一点字节——空基类优化(Empty Base Optimization, EBO)。写库时常会用到空类作为“策略/标签/无状态的行为对象”,EBO 能把这些没有状态的基类挤出对象布局,节省空间、提升局部性。
还是试试TL;DR¶
- EBO 允许编译器把空的基类子对象的存储省掉(即不占额外字节),从而减小派生类的 sizeof。
- 空成员变量默认不能被 EBO 压缩,但 C++20 引入的
[[no_unique_address]]可以对成员实现类似的压缩效果。 - 不要依赖对象地址唯一性去识别空子对象——地址可能相同(这是被允许的优化副作用),对地址的假设会导致 bug。
- 实战:库实现常用“继承空策略类”或“compressed pair”技巧;C++20 让事情更干净,但了解传统 EBO 仍然很有用。
概念从生活化的比喻说起¶
想象一个容器对象里有两个成员:一个是真正装东西的仓库(比如 int、指针),另一个是空的“标签”——仅代表行为,没有数据。直觉上你可能会给每个成员都分配空间,但语言标准允许编译器把“空标签”这个基类子对象放到不占额外空间的位置(比如复用派生对象的首字节)。这样派生对象整体更小,缓存更友好——这就是 EBO 的核心。
标准把“最派生对象必须有非零大小”的要求施加在最派生对象上,但基类子对象不受此限制,编译器可以把空基类子对象大小视为 0(即不占额外字节)。这正是 EBO 的法理学来源。
简单示例¶
struct Empty {}; // 空类
struct A {
Empty e; // 成员,通常会占 1 字节
int x;
};
struct B : Empty { // 继承 Empty —— EBO 有机会发生
int x;
};
static_assert(sizeof(A) >= sizeof(int) + 1);
static_assert(sizeof(B) == sizeof(int)); // 在支持 EBO 的编译器上通常成立
在上面例子中,A 中的 Empty e 是数据成员,按语言规则它需要占非零字节(以保证数组等语义);而 B 把 Empty 作为基类,编译器可以把它“压进”B 的布局中,从而 sizeof(B) 通常等于 sizeof(int)(不同编译器/ABI 可能细节不同)。
为什么 STL/库里常看到“继承空类”的套路?¶
标准库里,像分配器(allocator)、比较器(comparator)、删除器(deleter)等类型往往是无状态的空类。如果把它们作为成员,会浪费空间;把它们作为基类(通常是私有继承)可以启用 EBO,节省对象体积。很多实现把指针+空删除器这种情况包装成“compressed pair”或类似工具,以实现最小化的对象大小。微软的 STL 博客和其他实现说明了这种做法的普遍性。
C++20:[[no_unique_address]] 把“空成员优化”变得正式且安全¶
传统 EBO 只能通过继承实现(成员无法被压缩)。C++20 引入的 [[no_unique_address]] 属性允许成员也能被允许与其它子对象共享地址(即允许零大小语义),从而用成员语法就能达到类似 EBO 的效果,代码更直观、语义更清晰。例如:
这在实现上比私有继承更好看,也避免了继承带来的潜在接口暴露。cppreference 和一些实现文章对 [[no_unique_address]] 的语义与限制有总结,强烈建议在能用 C++20 的地方优先采用。
常见误解与踩坑(务必注意)¶
- “空类子对象一定没有地址”——错。 标准允许基类子对象与最派生对象共享起始地址;这会导致基类子对象的地址可能与其它子对象(或对象整体)相同。不要写依赖子对象地址唯一性的代码。
std::pair为何不能直接利用 EBO? 因为std::pair把first和second作为成员而不是空基类,因此传统 EBO 无法用于成员(除非使用[[no_unique_address]]或把实现改成 compressed-pair 风格)。这也是为啥有 “compressed pair” 之类的内部实现技巧。- 多个空基类有时会互相影响:若你从多个空类型继承,编译器会尝试为它们做 EBO,但在某些情况下(比如重复的基类类型、ABI 或嵌套模板导致的类型相同)会限制优化。常见的做法是让每个空基类的类型对编译器来说“独一无二”(例如通过模板参数化)以确保压缩生效。有人把这个问题称为“需要使基类类型分别化”。
实战建议¶
- 默认不用过早优化:把策略类写成空类、用成员或继承都行;可读性优先。
- 若需要最小内存或实现库(如智能指针、容器),优先考虑
[[no_unique_address]](C++20)或受控的私有继承 EBO 技巧。 C++20 能让代码更直观。 - 别依赖对象或子对象地址唯一性:在写调试、序列化或比较逻辑时,避免用地址来区分空子对象。地址可能会相同,标准允许这种重用。
小结¶
EBO 是 C++ 里一门“看得见效果却不显山露水”的微优化:让空策略类不再浪费字节。历史上我们用私有继承实现 EBO,现代 C++(C++20)通过 [[no_unique_address]] 让空成员也能被压缩,代码更直观更安全。实际工程里优先写清晰可维护的代码:当对象大小敏感时,再用 EBO / [[no_unique_address]] / compressed-pair 等技巧去手工优化,并在目标编译器上验证行为。