C语言中新增的20个属性no_unique_address有何具体含义?
摘要:有一个古老的c++问题:struct Empty{}; sizeof(Empty); 请问Empty的大小是多少。 很多新手会回答0,但稍有经验的开发者会说出正确答案,大小至少是1字节。 这看起来很奇怪,
有一个古老的c++问题:struct Empty{}; sizeof(Empty); 请问Empty的大小是多少。
很多新手会回答0,但稍有经验的开发者会说出正确答案,大小至少是1字节。
这看起来很奇怪,但这是语言规范决定的:c++要求同一类型的不同实例对象必须拥有完全不同的地址,如果Empty的大小是0,那么想象一下一个元素类型是Empty的数组,这个数组的连续存储空间里很可能不同的Empty会重叠在一起,从而导致它们违反前面对于拥有不同地址的规定。最简单最省事的做法就是让这种看起来大小应该为0的类型占据一字节的内存,从而确保每个实例都有独立的地址。而且语言规范也是要求这样去做的,它要求所有零大小的类型除了位域都必须占至少一字节的内存。
这么做当然带来了很多弊端,所以c++20新增了属性[[no_unique_address]]来解决问题。
不过在介绍这个属性之前,我们还得回顾一点基础知识。
基础回顾
c++的知识是一环套一环的,所以基础回顾环节少不了。我们需要回顾三个小知识点:什么是空类型、什么是空基类优化、空类型对内存对齐的影响。
首先回顾的是“空类型是什么”。
空类型,或者用语言规范里的叫法“zero size”,是指那些符合标准布局的、没有虚基类虚函数、没有非静态数据成员的类型。如果存在继承关系,则类型的每一层继承关系上涉及的类型也都必须符合前面提到的条件,这样的类型可以被视作是空类型。union不在此范围之内。
简单的说,下面三个类都可以被认为是空的:
struct A {
static constexpr int i = 0; // 这是静态数据成员,不影响类型为zero size
};
struct B {};
struct C: A {}; // 自己和基类都符合要求
int main()
{
static_assert(std::is_empty_v<A>);
static_assert(std::is_empty_v<B>);
static_assert(std::is_empty_v<C>);
}
std::is_empty是c++11新增的用于判断类型是否是zero size的接口。我们可以看到,没有非静态数据成员没有虚函数且基类也符合同样条件的类型都会被认为是空类型。
概念还是很容易理解的,不过标准并没有把话说死,在后面标准紧接着指出任何编译器觉得应该是空类型的东西也可以算作空类型。换句话说除了标准规定的少数情况,还有不少类型是否为空是具体平台和编译器共同影响的。
第二个要回顾的是“空类型对内存对齐的影响”。在复习空基类优化之前我们需要知道优化的动机,而动机来自于空类型对内存对齐的影响。
我们现在都知道因为c++对象地址的限制,空类型需要占用至少一字节的内存。这会让程序付出代价:
struct Empty {};
struct A {
long number;
Empty e;
};
static_assert(sizeof(A) > sizeof(long));
A的大小至少为2个long类型的大小。为什么呢,因为c++有内存对齐的规则,类的对齐长度以所有非静态数据成员中对齐长度最大的为准,这里我们有两个非静态数据成员,number和e,number的长度是sizeof(long),而它的对齐长度要求也是sizeof(long),e的长度和对齐要求都是1,sizeof(long)一定大于1,所以最后类型A要求每个字段都以sizeof(long)为基准进行对齐,作为最后一个字段的e,前面的字段number正好有一个long类型那么长,而自己后面又没有其他字段,按对齐要求这时候需要在自己后面填充sizeof(long) - 1个字节的填充物。最后A的整体大小会是两个long那么大。
实际上我们用不到Empty占用的内存里的内容,通常我们使用空类型是为了利用其类方法或者静态数据,但却要为了这一字节付出内存占用上的代价。类型变成两倍大意味着高速缓存里能存下的同类型数据至少减少一半,对于频繁访问这类数据的程序来说这是显著的性能损失。
c++为了践行“不支付不必要的运行时代价”,提出了EBO——空基类优化(Empty Base Optimization)这一方案。
空基类优化,是指当基类为空类型,派生类的第一个非静态数据成员的类型和基类不一样,继承不是虚拟继承的时候,这个空类型的基类可以不占用任何存储空间。
