空基类优化与no_unique_address属性

空基类优化与no_unique_address属性

空基类优化的定义及实例

空基类优化(EBO: Empty Base Optimization),就是允许空的基类子对象大小为零。

什么是空类呢?
任何内容都没有的类是空类;只拥有静态成员的类,也是空类。

在C++中,一个空类的对象会占用1个字节,目的是为了保证该类的不同对象拥有不同的地址。

当使用组合的方式,让一个类拥有了一个空类的对象时,由于内存对齐的要求,这个类对象的大小的增加可能不止1个字节。
但如果使用继承的方式,就可以应用空基类优化,使得继承类中空基类部分不占空间。

空基类优化并不是标准的强制要求,但主流编译器基本都实现了。

不过即使是使用继承的方式,也有一条需要注意:
如果首个非静态数据成员是一个空基类的对象,或者是某空基类的派生类的对象,则禁用空基类优化。
原因是,要求两个同类型基类子对象在最终派生类型的对象表示中必须拥有不同地址。

看程序:

#include <iostream>
using namespace std;
 
struct Base {}; // 空类, size = 1 

struct Composed {       // size = 8
    Base b;     // 组合,无空基类优化
    int i;
};

struct Derived1 : Base {  // size = 4
    int i;      // 典型的空基类优化
};

struct Derived2 : Base {  // size = 8
    int i;
    Base b; // 这个例子似乎有点特殊,解释可能是: 继承的空基类已被优化,则组合的空基类不能再被优化
};

struct Derived3 : Base {  // size = 8
    Base b; // 第一个非静态数据成员,因此对b不能应用空基类优化
    int i;
};
 
struct Derived4 : Base {  // size = 12 
    Derived1 c; // 第一个非静态数据成员,因此对c不能应用空基类优化 
    int i;
};

struct Derived5 : Base {  // size = 8
    int i;
    Derived1 c; // 不是第一个非静态成员,且派生自空基类,对c可应用空基类优化
};


 
int main()
{
    cout << "sizeof(Base) = " << sizeof(Base) << endl;          // 1 
    cout << "sizeof(Composed) = " << sizeof(Composed) << endl;  // 8 
    cout << "sizeof(Derived1) = " << sizeof(Derived1) << endl;  // 4
    cout << "sizeof(Derived2) = " << sizeof(Derived2) << endl;  // 8
    cout << "sizeof(Derived3) = " << sizeof(Derived3) << endl;  // 8
    cout << "sizeof(Derived4) = " << sizeof(Derived4) << endl;  // 12
    cout << "sizeof(Derived5) = " << sizeof(Derived5) << endl;  // 8
    
    return 0;
}

解释一下代码:

  • Composed类是典型的组合,无空基类优化,因此size是 4+4=8
  • Derived1类是典型的空基类优化, 因此size是 4+0=4
  • Derived2类比较特殊,它本身继承了一个空基类对象,又组合了一个空基类对象,因此可能对所组合的空基类对象无法做空基类优化,因此size是 4+4=8
  • Derived3类的第一个非静态数据成员是 Base b, 不能对其应用空基类优化,因此size是 4+4=8
  • Derived4类的第一个非静态数据成员是空基类派生类的对象,因此不能对 Derived1 c 应用空基类哟话,因此size是 (4+4)+4=12
  • Derived5类的第一个非静态数据成员是int,因此可以对 Derived1 c 应用空基类优化,因此size是 4+4=8

[[no_unique_address]] 属性(C++20)

本属性指示此数据成员的地址无须"和其类的所有其他非静态数据成员的地址都不同"
简而言之,就是指定了该属性的类的数据成员的地址,有可能和该类的其他非静态数据成员的地址相同。
这表示若该成员拥有空类型(例如无状态分配器),则编译器可将它优化为不占空间,正如同假如它是空基类一样。

看程序:

#include <iostream>
using namespace std;

struct Empty {}; // 空类
 
struct A {
    int i;
    Empty e;
};
 
struct B {
    int i;
    [[no_unique_address]] Empty e;
};
 
struct C {
    char c;
    [[no_unique_address]] Empty e1, e2;  // e1与e2只能优化掉一个
};
 
struct D {
    char c[2];
    [[no_unique_address]] Empty e1, e2;  
};
 
int main()
{
    cout << "sizeof(A) = " << sizeof(A) << endl;    // 8
    cout << "sizeof(B) = " << sizeof(B) << endl;    // 4
    cout << "sizeof(C) = " << sizeof(C) << endl;    // 2
    cout << "sizeof(D) = " << sizeof(D) << endl;    // 3
    
    return 0;
}

解释一下,

  • A是组合,无空基类优化,因此size是 8
  • B中e被指定了属性[[no_unique_address]],属于典型的强制空基类优化,因此size是 4
  • C中e1,e2都被指定了属性[[no_unique_address]],但它们不能共享同一个地址,因为它们拥有相同的类型,因此其中之一需与c共享地址;
  • 对于D,理论上e1与c[0]共享地址,e2与c[1]共享地址,所以理论上size是 2,但实际上这个要根据编译器的实现来看,笔者的是 g++ 11.2.0, D的size是3.

空基类优化的应用场景

空基类优化常用于具分配器的标准库类(std::vector、std::function、std::shared_ptr 等),使得当分配器无状态时可避免为其分配器成员占用任何额外存储。
这是通过将必要的数据成员之一(例如 vector 的 begin、end 或 capacity 指针)与分配器一起,在 boost::compressed_pair 的某种等价物中存储而实现的。

Reference

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值