C++ / a working model

30 / 80   ·   C++20   ·   约 8 分钟

空类、EBO 与 [[no_unique_address]]

先记住这句话

空类的完整对象仍具有非零大小,以支持对象身份和数组寻址;空基类子对象则可能不占额外空间。C++20 的 no_unique_address 将重叠布局机会扩展到成员,但不保证具体 sizeof,也不取消同类型对象的身份规则。

本篇内容
  1. 空的是状态,不是类型语义
  2. 空基类优化与组合的表达力
  3. 允许重叠不等于保证压缩
  4. 运行示例
  5. 动手练习

空的是状态,不是类型语义

没有非静态数据成员的类仍可以有成员函数、静态数据、类型别名和构造析构行为。对于普通空类,完整对象的 sizeof 至少为一,不保证必须等于一;数组中相邻元素必须能作为不同对象被寻址,因此不能把一组空对象都折叠成同一个数组元素。

带虚函数的类虽然也可能没有显式数据字段,却不属于通常用于空对象优化的那类 empty type。标准类型特征 is_empty 判断的也不是源码里有没有写 int 字段。空策略类型常用于比较器、分配策略和编译期标签,它们不携带状态仍有实际用途。

空基类优化与组合的表达力

空基类子对象不受普通完整对象最小占位的同样约束,实现在允许的布局下可以让它不增加额外空间,这称为空基类优化 EBO。一些标准布局条件还对地址关系提出要求,但不能把所有继承结构都简化成固定的大小等式。

如果复用关系仅仅是“拥有一个策略”,用继承只为节约空成员空间容易扭曲接口。C++20 的 [[no_unique_address]] 可标记非静态非位域成员,使它成为潜在重叠子对象;空策略因而有机会与其他成员共享地址,同时仍保持清晰的组合关系。

允许重叠不等于保证压缩

属性允许实现利用布局机会,不承诺指定的对象大小,也不保证成员一定与别的成员同地址。它还可能允许复用非空成员的尾部填充,所以不能自行把该成员的整个 sizeof 字节区域当作独占存储随意覆盖。

同一对象内两个同类型空成员仍需满足地址区分规则,不能因为都加了属性就认为可完全重合。示例只断言空完整对象非零、同类型空成员地址不同以及策略调用正确,不断言外层一定等于 int 大小。空间收益应在目标编译器与 ABI 上测量,并作为实现结果记录。

容易答错的地方

  • sizeof(Empty) 常为一只是常见结果;对齐要求等因素可以使它更大,跨平台代码不应硬编码。
  • no_unique_address 不延长生命周期,也不让你可以绕开对象规则覆盖共享存储;它是布局属性,不是手工内存管理许可证。

运行一个例子

最低标准 C++20 · 完整程序 · 下载 .cpp

#include <cassert>
#include <type_traits>

struct Empty {};
struct Identity {
    int operator()(int value) const { return value; }
};
struct ViaBase : Empty {
    int value = 3;
};
struct WithPolicy {
    [[no_unique_address]] Identity policy;
    int value = 7;
    int evaluate() const { return policy(value); }
};
struct TwoTags {
    [[no_unique_address]] Empty first;
    [[no_unique_address]] Empty second;
    int value = 1;
};

int main() {
    static_assert(std::is_empty_v<Empty>);
    static_assert(sizeof(Empty) >= 1);
    Empty array[2];
    assert(&array[0] != &array[1]);
    ViaBase inherited;
    WithPolicy composed;
    TwoTags tags;
    assert(inherited.value == 3);
    assert(composed.evaluate() == 7);
    assert(&tags.first != &tags.second);
}

在本地编译

g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread objects-empty-class.cpp -o example && ./example

预期结果

预期:正常退出、无输出;所有 assert 通过。

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

WithPolicy 的 sizeof 在某编译器上等于 sizeof(int),能否写成库的跨平台 static_assert?

查看参考答案

不应这样做。该观察说明此目标实现利用了空成员重叠,但属性允许而不强制这一具体布局。跨平台断言应约束真实语义,如策略调用结果;若库确有严格 ABI 大小要求,应限定支持平台、编译器与构建配置,并把大小检查视为该平台的专门契约,而不是 C++20 的一般保证。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录