C++ 的空基类优化

https://zh.cppreference.com/w/cpp/language/ebo

https://zh.cppreference.com/w/cpp/named_req/StandardLayoutType

https://zhuanlan.zhihu.com/p/259299672

标准布局类型

下面提到了标准布局类型,先看一下什么叫标准布局类型

C++ 标准布局类型是为了让结构体或类的内存布局尽可能地像 C 语言中的 struct,以方便与 C 代码交互、内存映射、或进行低级别的转换(如 reinterpret_cast

注:reinterpret_cast 可以进行完全无关系的类型之间的转换,重新解释每一个二进制位

下列类型统称为标准布局类型:

  • 标量类型
  • 标准布局类类型:
    • 所有非静态数据成员、基类都是标准布局类型
    • 所有非静态数据成员都具有相同的可访问性
    • 所有非静态成员必须都在同一个类中声明(不能一部分在基类,一部分在派生类)
    • 不能有与第一个非静态成员类型相同的基类
    • 没有虚函数或者虚基类
  • 上面两个类型的数组

空基类优化

空基类优化(Empty base optimization,EBO),即允许空的基类子对象大小为零

在 C++ 中,为保证同一类型的不同对象地址始终不同,要求任何对象或者成员子对象的大小至少为 1,即使该类型是空的类类型(除非带有 [[no_unique_address]],C++ 20)

然而,基类子对象不受这种限制

为什么会有空基类优化?保证 reinterpret_cast 的行为可预测:

struct A {
    int x;
};

A a;
int* p = reinterpret_cast<int*>(&a);  // 合法!指向 a.x

struct Base {};
struct B : Base {
    int x;
};
B b;
int* p = reinterpret_cast<int*>(&b);  // 如果 Base 占据了空间,p 并不是 &b.x

但是,如果首个非静态数据成员的类型与一个空基类的类型相同或者由该空基类派生,那么禁用空基类优化,因为要求两个同类型基类子对象在最终派生类型的对象表示中必须拥有不同地址,例如:

struct Base {}; // 空类
 
struct Derived1 : Base {
    int i;
};
 
struct Derived2 : Base {
    Base c; // Base,占用 1 个字节,后随对 i 的填充
    int i;
};
 
struct Derived3 : Base {
    Derived1 c; // 从 Base 派生,占用 sizeof(int) 个字节
    int i;
};
 
int main() {
    // 不应用空基类优化,
    // 基类占用 1 个字节,Base 成员占用 1 个字节
    // 后随 2 个填充字节以满足 int 的对齐要求
    static_assert(sizeof(Derived2) == 2 * sizeof(int));
 
    // 不应用空基类优化,
    // 基类占用至少 1 个字节加填充,
    // 以满足首个成员的对齐要求(其对齐要求与 int 相同)
    static_assert(sizeof(Derived3) == 3 * sizeof(int));
}

上面的情况并不是标准布局类型,所以并不需要空基类优化

在 STL 中,广泛使用了空基类优化,例如 std::vector

template<typename _Tp, typename _Alloc>
struct _Vector_base {
    ...
    struct _Vector_impl : public _Tp_alloc_type, public _Vector_impl_data { ... };
    ...
}


template<typename T, typename Alloc = std::allocator<T> >
class vector : protected _Vector_base<T, Alloc> { ... };

_Vector_base 继承了空间配置器,这个空间配置器是个空类。除此之外,纵观所有 STL 中的容器,都使用了空间配置器,但是几乎所有都不会在容器类中内含一个 allocator ,一般都是用继承的方式,这样通过 EBO 可以省下几个字节的空间