空基类优化empty base class optimization

1、为什么C++中不允许类的大小是0

class ZeroSizeT {};
ZeroSizeT z[10];
&z[i] - &z[j];

一般是用两个地址之间的字节数除以类型大小而得到的,而类型大小是0将会出问题

2、为什么有时多个类组成实例所占空间都是一样的

class Empty
{ };

class EmptyToo : public Empty
{ };

class EmptyThree : public EmptyToo
{ };

sizeof(Empty) : 1
sizeof(EmptyToo) : 1
sizeof(EmptyThree) : 1

空基类优化:只要不会与同一类型的另一个对象或子对象分配在同一地址,就不需要为其分配空间

1、为什么C++中不允许类的大小是0

class ZeroSizeT {};
ZeroSizeT z[10];
&z[i] - &z[j];

一般是用两个地址之间的字节数除以类型大小而得到的,而类型大小是0将会出问题

2、为什么有时多个类组成实例所占空间都是一样的

class Empty
{ };

class EmptyToo : public Empty
{ };

class EmptyThree : public EmptyToo
{ };

sizeof(Empty) : 1
sizeof(EmptyToo) : 1
sizeof(EmptyThree) : 1

空基类优化:只要不会与同一类型的另一个对象或子对象分配在同一地址,就不需要为其分配空间。

3、对于空基类优化,如何理解前提条件“只要不会与同一类型的另一个对象或子对象分配在同一地址”,以及why

class Empty
{ };

class EmptyToo : public Empty
{ };

class EmptyThree : public Empty, public EmptyToo
{ };

sizeof(Empty) : 1
sizeof(EmptyToo) : 1
sizeof(EmptyThree) : 2

NoEmpty的基类Empty和EmptyToo不能分配在同一地址空间,C++内存布局不允许相同类型的子对象偏移量相同。

对空基类优化进行限制的根本原因在于,我们需要能比较两个指针是否指向同一对象。由于指针几乎总是用地址作内部表示,因此必须保证两个不同的地址对应两个不同的对象。

更详细的:http://www.programlife.net/the-empty-base-class-optimization.html

附:

关于空基类优化的应用:在《C++ Templates The Complete Guide》e_CN的p_435中关于函数对象的组合问题,使用空基类优化的特性,将组合关系改成了继承关系。



What is empty class and why worth optimization

Empty class顾名思义就是空类,比如

[cpp] view plaincopyprint?

  1. class empty {};

这里empty显然是一个空类,什么成员都没有。但是空类不限于这种形式,对于只有成员函数,并且没有non-static data member的类,也可以是空类。

[cpp] view plaincopyprint?

  1. class empty
  2. {
  3. public:
  4. static void f();
  5. void f();
  6. };
  7. class empty_too : public empty {};
但是有一点需要注意,如果一个类或者他的基类中包含虚函数,那么该类就不是empty class,因为通常一个含有虚函数的类,都有一个vptr,所以就有了数据成员,虽然是隐藏的。 

对于父类是虚基类的情况也是一样,因为一般子类需要一个指针指向虚基类的子对象。

对于一个空类,它的大小不是0,因为如果是0,那么两个对象就会拥有相同的地址,这样就无法简单用地址区分两个对象了。通常一个空类的大小可能是1,也可能是4,这取决于编译器。

如果一个类,它包含空类成员子对象,那么这就会造成一定的空间浪费,而这个浪费是可以避免的,因为有些编译器实现了一种称为empty base class optimization的优化。

标准中提到了这种优化:

10/5 Derived classes

A base class subobject may be of zero size (clause 9); however, two subobjects that have the same class type and that belong to the same most derived object must not be allocated at the same address (5.10).

基类子对象可以是大小为0的,但是限制是,两个相同类型的子对象不能分配在相同的地址上。所以技巧就是通过从空类来继承是实现一定的优化。

[cpp] view plaincopyprint?

  1. class empty1 {};
  2. class empty2 {};
  3. class non_empty1
  4. {
  5. public:
  6. empty1 o1;
  7. empty2 o2;
  8. };
  9. class non_empty2
  10. : public empty1
  11. , public empty2
  12. {};
这里empty1,2是空类,通过继承,non_empty2的大小还是1。但是non_empty1的大小就是2。

需要注意的是,继承可能会带来对接口的影响,因为在泛型代码中,你不知道用户传入的类是否包含虚函数,如果包含虚函数,那么可能有一个与我们的类正好同名的虚函数,这样我们的函数就被虚化了。 
解决这个问题可以不直接从空类继承,而是创建一个中间类,并让这种类来继承空类,这样可以将影响限制在我们的中间类中。并将这个中间类的对象作为成员保存。

[cpp] view plaincopyprint?

  1. class empty {};
  2. class foo : public empty {}; // not always correct
  3. class foo
  4. {
  5. class bar : public empty {};
  6. // ok, the interface of foo is not affected by the inheritance from empty;
  7. };
在stl中,大量用到了函数对象,并且有许多函数对象是空的,如果大量存储这些函数对象也是会造成一定的浪费的(为什么要存储?假设一下,哈哈)。

在《C++ template metaprogramming》中有这样一个例子:有一个类,实现一个简单的复合函数f(g(x))

[cpp] view plaincopyprint?

  1. template<typename R, typename F, typename G>
  2. class composed_fg
  3. {
  4. public:
  5. composed_fg(const F& f, const G& g)
  6. : f(f)
  7. , g(g)
  8. {}
  9. template<typename T>
  10. R operator ()(const T& t) const
  11. {
  12. return f(g(t));
  13. }
  14. private:
  15. F f;
  16. G g;
  17. };
这里如果f或者g是空类,那么就会造成空间的浪费,视编译器而定,composed_fg最多可能会在32-bit平台上占用8字节。但是我们进行空基类优化,当f或者g中有空基类时,我们选择不同的实现。 

boost.compressed_pair就实现了一个优化过的std.pair,我们来分析一下boost.compressed_pair的实现。

compressed_pair根据T1, T2的类型,来选择不同的实现,有6种情况

T1 == T2 T1 empty T2 empty
false false false
false true false
false false true
false true true
true false false
true true true

其中区分T1==T2是因为,C++不允许有2个相同的直接基类。

What is empty class and why worth optimization
Empty class顾名思义就是空类,比如
[cpp] view plaincopyprint?
class empty {};
这里empty显然是一个空类,什么成员都没有。但是空类不限于这种形式,对于只有成员函数,并且没有non-static data member的类,也可以是空类。
[cpp] view plaincopyprint?
class empty
{
public:
    static void f();
    void f();
};  

class empty_too : public empty {};
但是有一点需要注意,如果一个类或者他的基类中包含虚函数,那么该类就不是empty class,因为通常一个含有虚函数的类,都有一个vptr,所以就有了数据成员,虽然是隐藏的。
对于父类是虚基类的情况也是一样,因为一般子类需要一个指针指向虚基类的子对象。
对于一个空类,它的大小不是0,因为如果是0,那么两个对象就会拥有相同的地址,这样就无法简单用地址区分两个对象了。通常一个空类的大小可能是1,也可能是4,这取决于编译器。
如果一个类,它包含空类成员子对象,那么这就会造成一定的空间浪费,而这个浪费是可以避免的,因为有些编译器实现了一种称为empty base class optimization的优化。
标准中提到了这种优化:
10/5 Derived classes
A base class subobject may be of zero size (clause 9); however, two subobjects that have the same class type and that belong to the same most derived object must not be allocated at the same address (5.10).
基类子对象可以是大小为0的,但是限制是,两个相同类型的子对象不能分配在相同的地址上。所以技巧就是通过从空类来继承是实现一定的优化。
[cpp] view plaincopyprint?
class empty1 {};
class empty2 {};  

class non_empty1
{
public:
    empty1 o1;
    empty2 o2;
};  

class non_empty2
    : public empty1
    , public empty2
{};
这里empty1,2是空类,通过继承,non_empty2的大小还是1。但是non_empty1的大小就是2。
需要注意的是,继承可能会带来对接口的影响,因为在泛型代码中,你不知道用户传入的类是否包含虚函数,如果包含虚函数,那么可能有一个与我们的类正好同名的虚函数,这样我们的函数就被虚化了。
解决这个问题可以不直接从空类继承,而是创建一个中间类,并让这种类来继承空类,这样可以将影响限制在我们的中间类中。并将这个中间类的对象作为成员保存。
[cpp] view plaincopyprint?
class empty {};
class foo : public empty {}; // not always correct  

class foo
{
    class bar : public empty {};
    // ok, the interface of foo is not affected by the inheritance from empty;
};
在stl中,大量用到了函数对象,并且有许多函数对象是空的,如果大量存储这些函数对象也是会造成一定的浪费的(为什么要存储?假设一下,哈哈)。
在《C++ template metaprogramming》中有这样一个例子:有一个类,实现一个简单的复合函数f(g(x))
[cpp] view plaincopyprint?
template<typename R, typename F, typename G>
class composed_fg
{
public:
    composed_fg(const F& f, const G& g)
        : f(f)
        , g(g)
    {}  

    template<typename T>
    R operator ()(const T& t) const
    {
        return f(g(t));
    }
private:
    F f;
    G g;
};
这里如果f或者g是空类,那么就会造成空间的浪费,视编译器而定,composed_fg最多可能会在32-bit平台上占用8字节。但是我们进行空基类优化,当f或者g中有空基类时,我们选择不同的实现。
boost.compressed_pair就实现了一个优化过的std.pair,我们来分析一下boost.compressed_pair的实现。
compressed_pair根据T1, T2的类型,来选择不同的实现,有6种情况
T1 == T2    T1 empty    T2 empty
false    false    false
false    true    false
false    false    true
false    true    true
true    false    false
true    true    true
其中区分T1==T2是因为,C++不允许有2个相同的直接基类。
这个是T1和T2都不为空的情况,这里只是简单地在对象中保存了2个成员对象。再来看一下其中一个为空的情况。
template <class T1, class T2>
class compressed_pair_imp<T1, T2, 1>
    : protected ::boost::remove_cv<T1>::type
{
public:
    typedef T1                                                 first_type;
    typedef T2                                                 second_type;
    typedef typename call_traits<first_type>::param_type       first_param_type;
    typedef typename call_traits<second_type>::param_type      second_param_type;
    typedef typename call_traits<first_type>::reference        first_reference;
    typedef typename call_traits<second_type>::reference       second_reference;
    typedef typename call_traits<first_type>::const_reference  first_const_reference;
    typedef typename call_traits<second_type>::const_reference second_const_reference;

    compressed_pair_imp() {}

    compressed_pair_imp(first_param_type x, second_param_type y)
        : first_type(x), second_(y) {}

    compressed_pair_imp(first_param_type x)
        : first_type(x) {}

    compressed_pair_imp(second_param_type y)
        : second_(y) {}

    void swap(::boost::compressed_pair<T1,T2>& y)
    {
        // no need to swap empty base class:
        cp_swap(second_, y.second());
    }
private:
    second_type second_;
};

这里T1为空,compressed_pair从T1继承了,然而T2还是作为成员保存起来了。还有一点变化就是swap中,只对second进行了操作,很显然,因为T1子对象是空的,swap没有意义。 
其他的情况类似了,所以可以自己去看boost的源码。

Side Note

VC中存在对Empty base class过度优化的情况,对于2个相同类型基类子对象的情况,在g++中,会生成2个字节大小的对象,而VC中只是1个字节的大小。

References

[1] The "Empty Member" C++ Optimization 
[2] Empty Base Class or Structure Assignment Operator May Corrupt Data 
[3] Understanding the Empty Base Optimization 
[4] 《C++ template metaprogramming》 
[5] Why is the size of an empty class not zero?

参考:http://blog.csdn.net/seizef/article/details/6168721

http://www.cppblog.com/qinqing1984/archive/2011/07/10/150584.aspx

时间: 2024-11-10 08:54:56

空基类优化empty base class optimization的相关文章

模板与继承之艺术——空基类优化

1.概念 C++中有“空”类的概念,这就意味着在运行期间其内部不好任何内存. 只包含类型的成员函数.非虚成员函数和静态数据成员的类为空类. 非静态的数据成员,虚函数和虚基类则在运行时期消耗存储空间. 2.空基类优化如下: #include<iostream> using namespace std; class Empty{ typedef int Int; }; class EmptyToo : public Empty {}; class EmptyThree : public Empty

[百度空间] [原] Empty base class optimization

最近遇到了一个诡异的问题, 数组的数据不对, 最后发现是两个类型的大小不一样导致的. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class alloc { public: void* operator new(size_t n){...} void operator delete(void* p) {...} };   class alloc2 { public: void* operator new(size_

C++ 空白基类最优化(EBO 或 EBCO)

对于c++中的一个空类 class X { }; 事实上并不是空的,sizeof(X)并不等于0, 一般的结果是1.每个X的对象都有一个隐晦的1 bytes,是被编译器安插进去的一个char,这样可以使得这个class的两个objects在内存中配置独一无二的地址. 当X作为另一个类的成员时,如: class A { public: X x; int a; }; 由于X占一个字节,int占4个字节,再加上编译器的alignment调整,sizeof(Y) = 8. 但是当一个类继承X时: cla

C#继承机制 访问与隐藏基类成员

(1) 访问基类成员 通过base 关键字访问基类的成员:   调用基类上已被其他方法重写的方法.  指定创建派生类实例时应调用的基类构造函数.  基类访问只能在构造函数.实例方法或实例属性访问器中进行. 从静态方法中使用 base 关键字是错误的. 示例:下面程序中基类 Person 和派生类 Employee 都有一个名为 Getinfo 的方法.通过使用 base 关键字,可以从派生类中调用基类上的 Getinfo 方法. using System ;public class Person

C++中虚基类

摘自<C++程序设计> 如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员. C++提供虚基类(virtual base class)的方法,使得在继承间接共同基类时只保留一份成员. 下面举例说明: 在如下的图中: Person类是Student和Teacher的基类,而Graduate类又继承自Student和Teacher类. 如果使用虚基类的话,Graduate将有两份age拷贝,两份gender拷贝,两份name

不可或缺 Windows Native (22) - C++: 多重继承, 虚基类

[源码下载] 作者:webabcd 介绍不可或缺 Windows Native 之 C++ 多重继承 虚基类 示例1.基类 1CppBase1.h #pragma once #include <string> #include "CppBaseVirtual.h" using namespace std; namespace NativeDll { // virtual 代表 CppBaseVirtual 是 CppBase1 的虚基类(虚基类是在声明派生类时,指定继承方式

C++调用基类的构造函数

基类的代码是 class base { public: base(int id); base(){}; virtual void toString(); protected: int id; }; base::base(int n) { printf("base constructor!\n"); id = n; } void base::toString() { cout<<"my id is "<<id<<endl; } 派生

VC++ 之 虚基类详解

在上一节中,有两个身份证号显然是不合理的.为此,可以把class Person这个共同基类设置为虚基类,这样,从不同路径继承来的同名数据成员在内存中就只有一个拷贝,同名函数也只有一种映射. 虚基类定义方式 虚基类(virtual base class)定义方式如下:    class 派生类名:virtual 访问限定符 基类类名{...};或:    class 派生类名:访问限定符 virtual 基类类名{...}; 其中:virtual 关键字只对紧随其后的基类名起作用. 例如:   

OOP1(定义基类和派生类)

面向对象程序设计基于三个基本概念:数据抽象,继承和动态绑定 数据抽象是一种依赖于接口和实现分离的编程技术.继承和动态绑定对程序的编号有两方面的影响:一是我们可以更容易地定义与其它类相似但不完全相同的类:二是在使用这些彼此相似的类编写程序时,我们可以在一定程度上忽略掉它们的区别. 在 c++ 语言中,当我们使用基类的引用或指针调用一个虚函数时将发生动态绑定 定义基类: 1 class Quote { 2 public: 3 Quote() = default; 4 Quote(const std: