为什么含Empty空结构体的std::tuple不同元素排布下大小不一致?
核心原因:空基优化的生效规则与std::tuple的固定实现结构
基础规则铺垫
- C++中的*空基优化(EBO)*仅对作为基类存在的空类型生效:这类空类(示例中的
Empty)被继承时不会占用额外的存储空间。但如果空类型是作为类的非静态成员变量存在的,必须占用至少1字节的独立存储空间,无法被优化为0大小。 - 主流C标准库(GCC的libstdc、Clang的libc++)中的
std::tuple并未采用平铺的成员布局,而是使用递归继承的链式结构实现:每层tuple节点对应一个元素,向外逐层继承持有剩余元素的节点。为了优化空元素的存储,每个元素的持有器会根据元素类型自动选择实现:如果元素是可EBO的空类型,持有器直接继承该空类型,本身成为独立的空类;如果元素是非空类型,持有器内部直接持有该类型的非静态成员,无法被EBO。
实际布局的大小推导
结合上述实现逻辑,在sizeof(std::byte)=1、对齐要求为1的平台上,三个示例tuple的大小来源可以逐层级推导:
1. sizeof(std::tuple<DByte, Empty, Empty>) = 2
这个tuple的元素顺序是[DByte, Empty, Empty],从递归结构的最深处向外看:
- 最深处的两个节点分别持有第二、第三个
Empty元素:由于Empty是可EBO的空类型,这两个节点都是独立的空类,在被外层继承时全部走EBO,不占任何存储空间。 - 最外层的节点持有第一个
DByte元素:DByte本身大小为2,由于继承的内层节点全部被EBO优化,整个tuple的大小就等于DByte的大小2,没有额外的空间占用。
2. sizeof(std::tuple<Empty, DByte, Empty>) = 3
这个tuple的元素顺序是[Empty, DByte, Empty],逐层推导:
- 最深处的节点持有最后一个
Empty元素:空类,走EBO不占空间。 - 中间层节点持有
DByte元素:DByte大小2,由于继承的最内层是空类,走EBO不占空间,本层节点大小为2。 - 最外层节点持有第一个
Empty元素:虽然持有器本身是空类,但由于它继承的中间层节点已经是大小为2的非空对象,EBO无法跨层优化本层的存储空间。这个空节点作为最外层的子对象,必须额外占用1字节的位置放在所有内层节点的后面,最终总大小为2+1=3。
3. sizeof(std::tuple<Empty, Empty, DByte>) =3
这个tuple的元素顺序是[Empty, Empty, DByte],逐层推导:
- 最深处的节点持有最后一个
DByte元素:DByte大小2,非空,无法被EBO。 - 中间层节点持有第二个
Empty元素:由于继承的最内层是大小为2的非空DByte节点,EBO无法生效,本层的空元素持有器需要在DByte的存储后面额外占1字节,累计大小3。 - 最外层节点持有第一个
Empty元素:空类,由于继承的中间层已经是大小为3的非空对象,本层走EBO不占额外空间,最终总大小维持3,与实测一致。
为什么无法实现设想的紧凑布局
理论上将三个tuple的大小都压缩到2字节确实可行(把DByte放在偏移0,两个Empty的存储和DByte重叠),但这种布局无法在现有tuple的实现框架下落地,核心限制有两个:
- C++的对象布局规则要求,派生类的所有直接基类子对象的地址必须严格按照声明顺序从低地址到高地址排布,不允许随意调换基类的位置。而tuple的递归继承顺序是固定的,无法为了省空间就把后面的非空元素挪到最前面的位置,否则会破坏
std::get<i>基于元素索引计算地址的核心契约。 - EBO的优化能力仅能让空基类的存储和它紧邻的外层/内层非空对象重叠,无法跨多个节点逆向挪动非空对象的位置来填满前面空出来的空间。
内容的提问来源于stack exchange,提问作者Bernardo Sulzbach
相关产品推荐
相关产品推荐

