25

我正在阅读有关空基优化(EBO)的信息。在阅读的过程中,我的脑海中突然出现了以下问题:

  1. 当 Empty 类对派生类没有任何贡献时(既不是功能方面的,也不是数据方面的),使用 Empty 类作为基类有什么意义?

  2. 这篇文章中,我读到:

//S 为空
类 struct T : S
{
      int x;
};

[...]

请注意,我们没有丢失任何数据或代码准确性:当您创建类型 S 的独立对象时,对象的大小仍然像以前一样为 1(或更大);只有当 S 被用作另一个类的基类时,它的内存占用才会缩小到零。为了实现这种节省的影响,想象一个包含 125,000 个对象的向量。仅 EBO 就节省了半兆内存!

这是否意味着如果我们不使用“S”作为“T”的基类,我们必然会消耗双倍兆字节的内存?我认为,这篇文章比较了两种我认为不正确的不同场景。

我想知道一个真实的场景,当 EBO 被证明是有用的时。(意味着,在同样的场景中,如果我们不使用 EBO,我们必然会不知所措!)

请注意,如果您的答案包含这样的解释:

关键是一个空类的大小不为零,但是当派生或派生它时,它的大小可以为零,那么我不是在问这个,因为我已经知道了。我的问题是,为什么有人首先会从一个空类中派生出他的类?即使他没有派生并且只是编写他的类(没有任何空基),他是否以任何方式不知所措?

4

9 回答 9

45

EBO 在基于策略的设计环境中很重要,您通常从多个策略类私下继承。如果我们以线程安全策略为例,可以想象一下伪代码:

class MTSafePolicy
{
public:
  void lock() { mutex_.lock(); }
  void unlock() { mutex_.unlock(); }

private:
  Mutex mutex_;
};

class MTUnsafePolicy
{
public:
  void lock() { /* no-op */ }
  void unlock() { /* no-op */ }
};

给定一个基于策略的设计类,例如:

template<class ThreadSafetyPolicy>
class Test : ThreadSafetyPolicy
{
  /* ... */
};

使用该类,MTUnsafePolicy只需不增加类的大小Test:这是一个完美的例子,不为你不使用的东西付费

于 2010-12-01T14:57:42.013 回答
7

EBO 并不是真正的优化(至少不是您在代码中所做的优化)。重点是空类的大小不为零,但是在派生或派生时,它的大小可以为零。

这是最常见的结果:

class A { };
class B { };

class C { };
class D : C { };

#include <iostream>
using namespace std;

int main()
{
        cout << "sizeof(A) + sizeof(B) == " << sizeof(A)+sizeof(B) << endl;
        cout << "sizeof(D) == " << sizeof(D) << endl;

        return 0;
}

输出:

sizeof(A) + sizeof(B) == 2
sizeof(D) == 1

编辑:优化是,如果你确实派生(例如从函子或只有静态成员的类),你的类的大小(即派生)不会增加 1(或由于填充字节,更可能是 4 或 8)。

于 2010-12-01T14:19:30.773 回答
6

EBO 中的“优化”意味着使用基类的情况可以优化为比使用相同类型的成员时使用更少的内存。即你比较

struct T : S 
{
      int x;
};

struct T
{
      S s;
      int x;
};

不与

struct T
{
      int x;
};

如果您的问题是您为什么会有一个空类(作为成员或作为基础),那是因为您使用了它的成员函数。空意味着它没有数据成员,而不是它根本没有任何成员。在使用模板进行编程时,经常会发生这样的事情,其中​​基类有时是“空的”(没有数据成员),有时不是。

于 2010-12-01T14:56:58.867 回答
5

当程序员想要在不增加客户端类大小的情况下向客户端公开一些数据时使用它。空类可以包含枚举和 typedef 或一些客户端可以使用的定义。使用它的类的最明智的方法是,私下继承这样的类。这将从外部隐藏数据,并且不会增加您的班级规模。

于 2012-10-23T18:24:35.037 回答
2

可以有没有任何成员变量的空类,但是可以充当实用程序类的成员函数(static或),让我们调用 this 。现在我们可以有一个案例,我们想要创建一个类(我们称之为),它与 具有包含类型的关系,但不是“is-a”关系。一种方法是创建一个类型为in的成员对象,如下所示:non staticEmptyClassSomeClassEmptyClassEmptyClassSomeClass

class EmptyClass  
{
public:
    void someFun1();
    static int someUtilityFun2();
};
//sizeof(EmptyClass) = 1


class SomeClass
{
 private:
    EmptyClass e;
    int x;
};
//sizeof(SomeClass) = 8

现在由于一些对齐要求,编译器可能会添加填充SomeClass,其大小现在为 8 字节。更好的解决方案是SomeClass私有派生EmptyClass,这样SomeClass就可以访问所有成员函数,EmptyClass并且不会通过填充增加额外的大小。

class SomeClass : private EmptyClass
{
private:
    int x;
} 
//sizeof(SomeClass) = 4
于 2021-07-31T16:42:44.187 回答
1

大多数时候,空基类要么被多态地使用(文章提到),作为“标记”类,要么作为异常类(尽管这些通常派生自非空的 std::exception)。有时有充分的理由开发以空基类开头的类层次结构。

Boost.CompressedPair 使用 EBO 在元素之一为空的情况下缩小对象的大小。

于 2010-12-01T14:36:15.257 回答
1

EASTL对他们为什么需要 EBO 有一个很好的解释,在他们链接到/信用的论文中也有深入的解释

于 2010-12-01T14:45:00.677 回答
0

EBO 不是程序员影响的东西,和/或如果程序员选择不从空基类派生,他将受到惩罚。

编译器控制是否:

class X : emptyBase { int X; };
class Y { int x };

你得到sizeof(X) == sizeof(Y)与否。如果你这样做,编译器会实现 EBO,如果没有,它不会。

永远不会发生任何情况sizeof(Y) > sizeof(X)

于 2010-12-01T14:20:56.633 回答
-1

我能想到的主要好处是dynamic_cast。您可以获取指向 S 的指针并尝试将其动态转换为从 S 继承的任何内容 - 假设 S 提供了像虚拟析构函数这样的虚函数,它几乎必须作为基类来做。例如,如果您正在实现一种动态类型语言,您可能希望或需要每个类型都从基类派生,纯粹是为了类型擦除存储和通过 dynamic_cast 进行类型检查。

于 2010-12-01T14:52:36.437 回答