空の基本クラスもメンバー変数であるときに、空の基本最適化が禁止されているのはなぜですか?


14

空のベース最適化は素晴らしいです。ただし、次の制限があります。

同じ基本型の2つの基本サブオブジェクトはオブジェクト表現内で異なるアドレスを持つ必要があるため、空の基本クラスの1つが最初の非静的データメンバーの型の型または基本でもある場合、空の基本最適化は禁止されています。最も派生したタイプの。

この制限を説明するには、次のコードを検討してください。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!");

私はこの振る舞いを完全に理解しています。私が理解していないのこの特定の動作が存在する理由です。見落としではなく明示的な追加であるため、明らかに理由で追加されました。これの根拠は何ですか?

特に、2つの基本サブオブジェクトが異なるアドレスを持つ必要があるのはなぜですか?上記のBarはタイプでfooあり、そのタイプのメンバー変数です。Barの基本クラスがのタイプの基本クラスと関係がある理由foo、またはその逆の理由がわかりません。

実際、私はどちらかと言えば、それを含むインスタンス&fooのアドレスと同じであることを期待しBarます。他の状況で必要になるためです(1)。結局のところ、私はvirtual継承についてBase2特別なことは何もしていません。基本クラスは関係なく空であり、を使用したコンパイルは、この特定のケースでは何も壊れないことを示しています。

しかし、明らかにこの推論はどういうわけか間違っており、この制限が必要になる他の状況があります。

答えがC ++ 11以降であるとしましょう(私は現在C ++ 17を使用しています)。

(1)注:EBOはC ++ 11でアップグレードされ、特にに対して必須になりましたStandardLayoutType(ただしBar、上記でははではありませんStandardLayoutType)。


4
引用した理論的根拠(「同じタイプの2つの基本サブオブジェクトは異なるアドレスを持つ必要があるため」)はどのように不十分ですか?同じタイプの異なるオブジェクトは異なるアドレスを持つ必要があり、この要件により、その規則に違反しないことが保証されます。空のベースの最適化は、ここで適用した場合、我々は持っている可能性Base *a = new Bar(); Base *b = a->foo;a==bはなく、aかつb明確に(おそらく別の仮想メソッドのオーバーライドと)異なるオブジェクトです。
Toby Speight

1
言語弁護士の回答は、仕様の関連部分を引用しています。そして、あなたはすでにそれについて知っているようです。
Deduplicator

3
ここであなたがどのような答えを探しているのか理解できません。C ++オブジェクトモデルは、それが何であるかです。オブジェクトモデルが必要とするため、制限があります。それ以上にあなたは何を探していますか?
Nicol Bolas

@TobySpeight 同じタイプの異なるオブジェクトには異なるアドレスが必要です明確に定義された動作を持つプログラムでこのルールを破ることは簡単に可能です。
言語弁護士

@TobySpeightいいえ、私はあなたが寿命について言うのを忘れたという意味ではありません:「同じタイプの別のオブジェクトは、寿命が尽きています。同じタイプの複数のオブジェクトを同じアドレスですべて有効にすることができます。これを可能にする文言には少なくとも2つのバグがあります。
言語弁護士

回答:


4

わかりました。いつも間違っていたようです。すべての私の例では、ベースオブジェクトのvtableが存在する必要があるため、空のベース最適化を開始できません。これらの例は、一意のアドレスが通常持つのに適している理由のいくつかの興味深い例を示していると思います。

この全体を詳細に検討した結果、最初のメンバーが空の基本クラスと同じ型である場合に、空の基本クラスの最適化を無効にする技術的な理由はありません。これは、現在のC ++オブジェクトモデルのプロパティです。

しかし、C ++ 20では[[no_unique_address]]、非静的データメンバーに一意のアドレスは必要ない場合があることをコンパイラーに伝える新しい属性があります(技術的には、[intro.object] / 7が重複し ている可能性があります)。

これは、(私の強調)

非静的データメンバーは、別の非静的データメンバーのアドレスまたは基本クラスのアドレスを共有できます[...]

したがって、最初のデータメンバーに属性を与えることにより、空の基本クラス最適化を「再アクティブ化」できます[[no_unique_address]]。これ(および私が考えることができる他のすべてのケース)がどのように機能するかを示す例をここに追加しました。

これによる問題の間違った例

空のクラスには仮想メソッドがない可能性があるため、3つ目の例を追加しましょう。

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

しかし、最後の2つの呼び出しは同じです。

古い例(空のクラスには仮想メソッドが含まれていない可能性があるため、おそらく質問には答えないでください)

上記のコードで(仮想デストラクタを追加して)次の例を検討してください。

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.

しかし、コンパイラはこれらの2つのケースをどのように区別すべきですか?

そして、おそらく少し不自然です:

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

しかし、空の基本クラスの最適化があれば、最後の2つは同じです!


1
ただし、2番目の呼び出しの動作は未定義であると主張することもできます。したがって、コンパイラは何も区別する必要はありません。
StoryTeller-Unslander Monica

1
仮想メンバーを持つクラスは空ではないので、ここでは関係ありません。
Deduplicator

1
@Deduplicatorその上で標準的な引用がありますか?Cpprefは、空のクラスは「非静的データメンバーを持たないクラスまたは構造体」であることを示しています。
n314159

1
cnpreferenceの@ n314159 std::is_emptyははるかに複雑です。eel.isの現在のドラフトと同じです。
Deduplicator

2
dynamic_castポリモーフィックでない場合はできません(ここでは関係のない小さな例外があります)。
TC
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.