当空基类也是成员变量时,为什么禁止空基优化?


14

空基优化非常棒。但是,它具有以下限制:

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

要解释此限制,请考虑以下代码。该static_assert会失败。而将Foo或更改为或Bar从继承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,反之亦然。

确实,如果有的话,我希望它&fooBar包含它的实例的地址相同,因为在其他情况下它是必需的(1)。毕竟,我对virtual继承没有任何幻想,无论如何基类都是空的,并且带有的编译Base2显示在这种特殊情况下没有任何中断。

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

假设答案应该针对C ++ 11或更高版本(我目前正在使用C ++ 17)。

(1)注意:EBO在C ++ 11中进行了升级,并且特别成为StandardLayoutTypes的必需项(尽管Bar,上面不是StandardLayoutType)。


4
您引用的基本原理(“ 因为两个相同类型的基本子对象必须具有不同的地址 ”)是如何实现的?要求相同类型的不同对象具有不同的地址,并且此要求确保我们不会违反该规则。如果在此处应用空基础优化,则可以Base *a = new Bar(); Base *b = a->foo;使用a==b,但是ab显然是不同的对象(也许具有不同的虚拟方法覆盖)。
Toby Speight

1
语言律师的答案引用了规范的相关部分。看来您已经知道了。
重复数据删除器

3
我不确定我在这里想要什么答案。C ++对象模型就是它的本质。存在限制是因为对象模型需要它。除此之外,您还在寻找什么?
Nicol Bolas

@TobySpeight 要求相同类型的不同对象具有不同的地址很容易在行为明确的程序中破坏此规则。
语言律师

@TobySpeight不,我不是说您忘记谈论生命周期:“具有相同类型且寿命不同的不同对象”。在同一个地址上可能有多个相同类型的对象,它们都处于活动状态。措辞中至少有2个bug允许这样做。
语言律师

Answers:


4

好的,似乎我一直都错,因为对于我的所有示例,都需要为基础对象存在一个vtable,这将阻止从空的基础优化开始。我让这些示例站起来,因为我认为它们提供了一些有趣的示例,说明为什么唯一的地址通常是一件好事。

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

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

这意味着(强调我的)

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

因此,可以通过为第一个数据成员赋予属性来“重新激活”空的基类优化[[no_unique_address]]。我在这里添加了一个示例,该示例显示了此(以及我能想到的所有其他情况)的工作方式。

通过这个错误的例子问题

由于似乎空类可能没有虚方法,因此让我添加第三个示例:

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

但是如果我们有空的基类优化,则最后两个是相同的!


1
有人可能会辩称,第二个调用仍然具有未定义的行为。因此,编译器无需区分任何内容。
StoryTeller-Unslander Monica

1
具有任何虚拟成员的类不能为空,所以这里无关紧要!
重复数据删除器

1
@Deduplicator您对此有标准报价吗?Cppref告诉我们,空类是“没有非静态数据成员的类或结构”。
n314159

1
std::is_empty有关cppreference的@ n314159 更为详尽。与eel.is上的当前草案相同
重复数据删除器

2
dynamic_cast当它不是多态的时(除了此处不相关的少数例外),您不能这样做。
TC
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.