C/C++编程:空基类优化

C++类常为”空“,这就意味着在运行期其内部表示不耗费任何内存。这常见于只包含类型成员,非虚成员函数和静态数据成员的类,而非静态数据成员、虚函数和虚基类会在运行期耗费内存

即使是空类,其大小也不会为0

#include 

class EmptyClass{
};

class EmptyClass1{
    static  int i;
};
int EmptyClass1::i = 1;


class EmptyClass2{
    static  int i;
    void test(){};
};

class EmptyClass3{
    typedef int Int;
};

class NoEmptyClass{
    int i = 0;
};
int main(){
    printf("%lu\n", sizeof(EmptyClass));
    printf("%lu\n", sizeof(EmptyClass1));
    printf("%lu\n", sizeof(EmptyClass2));
    printf("%lu\n", sizeof(EmptyClass3));
    printf("%lu\n", sizeof(NoEmptyClass));
}

在这里插入图片描述

布局原则

C++的设计者不允许类的大小为0,其原因有很多,比如由它们构成的数组,其大小必然也是0,这会导致指针运算中普遍使用的性质失效。比如,假设类型ZeroSizedT的大小为0,则下面的操作会出现错误:

    ZeroSizedT  z[10];
    auto v =  &z[9]  - &z[2];  // 计算两个指针或者地址之间的距离

通常而言,上面的差值,一般是用两个地址之间的字节数除以类型大小而得到的,而类型大小为0就不妙了。

虽然不存在0大小的类,但这扇门也没有彻底关死。C++规定,当空类作为基类时,只要不会与同一类型的另一个对象或子对象分配在同一地址,就不需要为其分配任何空间。这个就叫做空基类优化技术。看个例子:

#include 

class EmptyClass{
};

class EmptyFoo : public EmptyClass{

};
class EmptyThree : public EmptyFoo{

};
int main(){
    printf("%lu\n", sizeof(EmptyClass));
    printf("%lu\n", sizeof(EmptyFoo));
    printf("%lu\n", sizeof(EmptyThree));
}

如果编译器支持空基类优化,上面程序的所有输出结果相同,但是均不为0。也就是说,在类EmptyFoo 中的类 EmptyClass没有分配空间 。 如下图:
C/C++编程:空基类优化_第1张图片
如果不支持空基类优化,上面程序的输出结果不同。布局如下图:
C/C++编程:空基类优化_第2张图片
再看个例子:

#include 

class EmptyClass{
};

class EmptyFoo : public EmptyClass{

};
class NoEmpty :public EmptyClass,  public EmptyFoo{

};

int main(){
    printf("%lu\n", sizeof(EmptyClass));
    printf("%lu\n", sizeof(EmptyFoo));
    printf("%lu\n", sizeof(NoEmpty));
}

在这里插入图片描述
NoEmpty 为什么不为空类呢?这是因为NoEmpty 的基类EmptyClass和EmptyFoo 不能分配到同一地址空间,否则EmptyFoo 的基类EmptyClass和NoEmpty 的EmptyClass会撞到同一地址空间上。换句话说,两个相同类型的子对象偏移量相同,这是C++布局规则不允许的
C/C++编程:空基类优化_第3张图片
对空基类优化进行限制的根据原因在于:我们需要能比较两个指针是否指向同一对象。由于指针几乎总是用地址内部表示,所以我们必须保证两个不同的地址(即两个不同的指针)对应两个不同的对象

成员作基类

对于数据成员,则不存在类似空基类优化的技术,否则遇到指向成员的指针时就会出现问题。因此我们可以考虑将成员变量实现为(私有)基类的形式。

在模板中考虑这个问题非常有必要,因为模板参数常常可能是空类;但是对于一般情况,我们并不能依赖这条规则(即模板参数常常可能是基类);而且如果对某一个模板参数一无所知,也不能很容易就实现空基类优化。看个例子:

template<typename T1, typename T2>
class MyClass{
	private:
		T1 a;
		T2 b;
};

模板参数T1和T2之一或全部,都有可能是空类,那MyClass就不能得到最优布局,每个这样的实例就可能会浪费一个字的内存。

把模板参数直接作为基类可以解决这个问题:

template<typename T1, typename T2>
class MyClass : private T1, private T2{};

但是,比如当T1和T2是int这样的基本类型时,上面的做法很有问题;另外,当T2和T2类型相同时,也会出问题(这个问题可以用模板特化或者通过添加中间层进行继承的方法[示例:模板的命名参数]解决);还有一个很大的问题就是增加基类会改变接口;还有一个问题就是继承模板参数甚至能影响到成员函数是否为虚。显然,引入EBCO会引来很多不必要的麻烦

如果一个已知的模板参数的类型必然是类,该模板的另一个成员类型不是空类,那么有一个方法更加可行,大概相反是借助EBCO,把可能为空的类型参数与这个成员”合“起来,比如对于:

template<typename CustomClass>
class Optimizable{
	CustomClass info; // 可能为空
	void * storage;
};

可以将其改写为:

template<typename CustomClass>
class Optimizable{
	private:
		BaseMemberPair<CustomClass, void *> info_and_storage;
}

虽然实现可能麻烦了,但是性能可以显著提高
BaseMemberPair 的实现如下:

template<typename Base, typename Member>
class BaseMemberPair : private Base{
private:
    Member member;
public:
   // 构造函数
    BaseMemberPair(Base const & b, Member const & m) : Base(b), member(m) {
    }

   // 通过first()来访问基类数据
    Base const & first() const {
        return (Base const  &) *this;
    }
    Base & first()  {
        return (Base   &) *this;
    }

 // 通过second()来访问基类的成员变量
    Member const & second() const {
        return this->member;
    }
    Member & second()  {
        return this->member;
    }
};

封装在BaseMemberPair中的数据成员(其存储方式在Base为空时可得到优化),需要通过成员函数first和second访问

你可能感兴趣的:(C++)