【问题标题】:Why is empty base optimization forbidden when the empty base class is also a member variable?当空基类也是成员变量时,为什么禁止空基优化?
【发布时间】:2020-04-26 01:57:25
【问题描述】:

Empty base optimization 很棒。但是,它具有以下限制:

如果空基类中的一个也是第一个非静态数据成员的类型或类型的基类,则禁止空基类优化,因为相同类型的两个基子对象需要在其中具有不同的地址派生最多的类型的对象表示。

要解释此限制,请考虑以下代码。 static_assert 将失败。然而,将FooBar 更改为从Base2 继承将避免错误:

#include <cstddef>

struct Base  {};
struct Base2 {};

struct Foo : Base {};

struct Bar : Base {
    Foo foo;
};

static_assert(offsetof(Bar,foo)==0,"Error!");

我完全理解这种行为。我理解的是为什么这种特殊行为存在。它显然是有原因的,因为它是明确的添加,而不是疏忽。这样做的理由是什么?

特别是,为什么要求两个基本子对象具有不同的地址?在上面,Bar 是一个类型,foo 是该类型的一个成员变量。我不明白为什么Bar 的基类对foo 类型的基类很重要,反之亦然。

确实,如果有的话,我希望&amp;foo 与包含它的Bar 实例的地址相同——因为它在其他情况下是必需的(1) .毕竟,我并没有对virtual 继承做任何花哨的事情,基类无论如何都是空的,而使用Base2 的编译表明在这种特殊情况下没有任何问题。

但显然这种推理在某种程度上是不正确的,并且在其他情况下需要这种限制。

假设答案应该适用于 C++11 或更新版本(我目前使用的是 C++17)。

(1) 注意:EBO 在 C++11 中得到了升级,特别是对于 StandardLayoutTypes(尽管上面的 Bar 是不是StandardLayoutType)。

【问题讨论】:

  • 你引用的理由(“因为相同类型的两个基本子对象需要有不同的地址”)有什么不足?相同类型的不同对象需要有不同的地址,这个要求确保我们不会违反这个规则。如果此处应用空基优化,我们可以将Base *a = new Bar(); Base *b = a-&gt;foo;a==b 结合使用,但ab 显然是不同的对象(可能具有不同的虚拟方法覆盖)。
  • 语言律师的回答是引用规范的相关部分。看来你已经知道了。
  • 我不确定我是否理解您在这里寻找什么样的答案。 C++ 对象模型就是这样。存在限制是因为对象模型需要它。除此之外,您还在寻找什么?
  • @TobySpeight 相同类型的不同对象需要有不同的地址在一个行为明确的程序中很容易打破这个规则。
  • @TobySpeight 不,我不是说你忘了说生命周期:“同一类型的不同对象在它们的生命周期内 .在同一个地址可以有多个相同类型的对象,它们都是活动的。允许这样做的措辞中至少有 2 个错误。

标签: c++ class inheritance language-lawyer memory-layout


【解决方案1】:

好吧,好像我一直都做错了,因为对于我的所有示例,都需要存在一个用于基础对象的 vtable,这将防止空基础优化开始。我将保留这些示例,因为我认为它们提供了一些有趣的示例,说明为什么拥有唯一地址通常是一件好事。

在更深入地研究了这一点之后,当第一个成员与空基类的类型相同时,没有技术理由禁用空基类优化。这只是当前 C++ 对象模型的一个属性。

但是对于 C++20,会有一个新属性 [[no_unique_address]] 告诉编译器非静态数据成员可能不需要唯一地址(从技术上讲,它可能重叠 @987654321 @)。

这意味着(强调我的)

非静态数据成员可以共享另一个非静态数据成员的地址或基类的地址,[...]

因此可以通过给第一个数据成员属性[[no_unique_address]]来“重新激活”空基类优化。我添加了一个示例 here,它展示了这个(以及我能想到的所有其他情况)是如何工作的。

问题的错误示例

既然空类似乎没有虚方法,那我再补充第三个例子:

int stupid_method(Base *b) {
  if( dynamic_cast<Foo*>(b) ) return 0;
  if( dynamic_cast<Bar*>(b) ) return 1;
  return 2;
}

Bar b;
stupid_method(&b);  // Would expect 0
stupid_method(&b.foo); //Would expect 1

但是最后两个调用是一样的。

老例子(可能不回答这个问题,因为空类可能不包含虚拟方法,似乎)

在上面的代码(添加了虚拟析构函数)中考虑以下示例

void delBase(Base *b) {
    delete b;
}

Bar *b = new Bar;
delBase(b); // One would expect this to be absolutely fine.
delBase(&b->foo); // Whoaa, we shouldn't delete a member variable.

但是编译器应该如何区分这两种情况呢?

也许不那么做作:

struct Base { 
  virtual void hi() { std::cout << "Hello\n";}
};

struct Foo : Base {
  void hi() override { std::cout << "Guten Tag\n";}
};

struct Bar : Base {
    Foo foo;
};

Bar b;
b.hi() // Hello
b.foo.hi() // Guten Tag
Base *a = &b;
Base *z = &b.foo;
a->hi() // Hello
z->hi() // Guten Tag

但如果我们有空基类优化,最后两个是一样的!

【讨论】:

  • 尽管如此,有人可能会争辩说第二次调用具有未定义的行为。所以编译器不需要区分任何东西。
  • 具有任何虚拟成员的类不为空,所以这里无关紧要!
  • @Deduplicator 您对此有标准报价吗? Cppref 告诉我们空类是“没有非静态数据成员的类或结构”。
  • @n314159 std::is_empty 在 cppreference 上要详细得多。与当前草案相同on eel.is
  • 当它不是多态的时候你不能dynamic_cast(这里不相关的小例外)。
猜你喜欢
  • 1970-01-01
  • 2011-05-18
  • 2021-11-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多