空对象优化(Empty Base Optimization,简称 EBO)是 C++ 编译器的一种 优化技术,用于消除空类作为基类时占用的内存空间,从而避免浪费空间、提升结构体或类的存储效率。
1 什么是“空对象”?
一个**空类(empty class)**是指:
class Empty {};
它没有任何数据成员(包括虚函数、虚继承等),仅有默认的构造/析构函数。根据 C++ 的标准:
每个对象都必须具有唯一的地址,即使它不包含任何数据。
所以如果你直接定义:
Empty e1, e2;
编译器仍然会让 sizeof(Empty) >= 1
,通常为 1 字节,以保证地址唯一性。
2 空对象优化(EBO)原理
空类作为基类时,如果派生类没有其它成员使用其地址,那么可以省略它占用的空间。
示例:空对象优化前后对比
// 不继承空类:直接成员,浪费空间
class Empty {};
struct NotOptimized {
int x;
Empty e;
};
// 使用继承:可以应用 EBO
struct Optimized : Empty {
int x;
};
对比 sizeof
:
std::cout << sizeof(NotOptimized) << "\n"; // 可能是 8(int + 空类占1 + padding)
std::cout << sizeof(Optimized) << "\n"; // 通常是 4(只包含 int)
结果说明:
NotOptimized
:包含一个Empty
成员,因此占用额外空间;Optimized
:Empty
作为基类,如果没有虚函数/虚继承,编译器可以完全消除其空间。
3 EBO 的典型应用场景
标准库容器优化
STL 中 std::function
、std::tuple
、std::optional
等结构都大量使用了 EBO。
template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
T* ptr;
Deleter deleter; // 如果 Deleter 是空类,会占额外空间
};
如果不优化,unique_ptr<int, EmptyDeleter>
也会占两个指针大小(ptr + deleter
),但 EBO 可以优化掉 deleter
空类的空间占用。
函数对象封装/策略模式
template<typename F>
struct FuncWrapper : F {
void operator()() { F::operator()(); }
};
如果 F
是空类,比如一个无状态 lambda 或策略类,EBO 可以让 FuncWrapper
大小为 1 或 0 字节。
4 EBO 的限制
有虚函数或虚继承
struct Base {
virtual void f() {}
};
- 虚表指针会阻止 EBO。
多个相同空基类
struct A {};
struct B1 : A {};
struct B2 : A {}; // 编译器不能合并两个 A 的地址
struct C : B1, B2 {};
C++ 要保证每个 A
的子对象地址不同,因此不能优化掉两个 A
。
5 C++20 的 [[no_unique_address]]
为了手动控制 EBO,C++20 引入了 [[no_unique_address]]
属性:
template<typename T, typename Deleter>
struct SmartPtr {
T* ptr;
[[no_unique_address]] Deleter deleter; // 允许优化
};
- 即使
Deleter
是空类,编译器也不会强行分配空间; - 可应用于非基类成员(突破传统 EBO 只能作用于基类的限制);
- 可读性和语义性更好。
总结
项目 | 是否允许空对象优化 |
---|---|
空类作为成员 | ❌ 不允许,至少占 1 字节 |
空类作为非虚基类 | ✅ 可以优化掉空间 |
空类有虚函数/虚继承 | ❌ 无法优化(有 vptr) |
多继承多个相同空基类 | ❌ 需保留唯一地址,不能优化 |
C++20 [[no_unique_address]] | ✅ 可以显式允许成员优化 |