前言
本文的目的在于用尽可能少的字数及版面下,直观地展示空基类优化(EBO/EBCO)的作用。
为什么要使用空基类优化
C++标准规定对象的大小不能为0。标准要求每个对象在内存中必须占有一定的空间,这意味着即使这个类没有任何成员,sizeof(A)也不会为0,那么:
class A
{
/* some functions here */
};
class B
{
A a;
int i;
}; // sizeof(B) > sizeof(int)
类型A虽然没有任何数据成员,但是它的对象仍然占有一定空间,但是,对于只调用类型A的函数的使用者而言,这个空间是没有用处的。而且因为内存对齐的原因,类型B的实际大小会对齐到两个int(甚至更多),因此我们需要想办法去除掉这一“多余”的空间。
使用空基类优化
使用场景1:allocator
不考虑多继承(详见延伸)的情况下,通过继承空类,我们就能避免这个空类占用空间:
class A {};
class B {
struct A_ : public A {
int i;
};
A_ a_;
}
此时sizeof(A_)=sizeof(int),此时派生类A_中类型A的对象不占内存空间。
上面的代码中我们可以看出,空基类优化的使用场景之一就是通过非空类型继承获得空基类的函数,利用派生类的对象来调用这些函数
举个标准库的例子:
class basic_string {
struct _Alloc_hider : allocator_type {
/* ctors... */
pointer _M_p;
};
_Alloc_hider _M_dataplus;
/*
如果不用空基类优化,就是:
pointer _M_p;
allocator_type _M_alloc;
那么sizeof(basic_string)就会大于等于(考虑到对齐)
sizeof(pointer) + sizeof(alloctor_type) + ...
使用空基类优化之后,就少了这一个sizeof(alloctor_type)或者对齐字节的大小
*/
};
就是利用_Alloc_hider来继承allocator_type这一可能的空基类,来优化基类的内存占用,从而避免内存浪费。为了让_Alloc_hider非空而又不增大basic_string的体积,就将存储字符串地址的_M_p放在了basic_string::_Alloc_hider而非basic_string中。
使用场景2:compressed_pair
一般pair的实现是这样的:
template<class T1, class T2>
struct pair // maybe inherits someting
{
T1 first;
T2 second;
};
同时将T1和T2类型的对象作为数据成员保存,那么就出现了如下问题:
如果T1是空类,T2的大小为4,那么由于字节对齐,实际上整个pair的大小为8,而我们期望这个pair的大小应该只是4。
而compressed_pair的出现就是为了解决这一问题:
template<typename T, bool is_first>
struct compressed_element
{
T& value() { return val; }
const T& value() const { return val; }
private:
T val;
};
template<class T1, class T2>
struct compressed_pair
: compressed_element<T1, true>, // first_base
compressed_element<T2, false> // second_base
{
T1& get_first()
{
return static_cast<first_base&>(*this).value();
}
T2& get_second()
{
return static_cast<second_base&>(&this).value();
}
};
只要T1、T2大小为0,那么由于compressed_element是基类,所以它的大小也为0,这样就优化了空类所占用的空间。
至于使用compressed_element的原因,一部分是为了避免T1、T2无法直接继承,另一部分是为了避免T1、T2共同继承某个类所造成的菱形继承。当然,以上的代码只展示了EBO相关的部分,具体其他细节与问题可以参阅这篇文章。
相关延伸
C++编程技巧: EBCO,知乎#利用EBCO来优化Tuple,#不满足EBCO原则的空类继承
C/C++编程:空基类优化,CSDN
cppreference.com/cpp/language/ebo