先に進む前に、注意:これは純粋に言語弁護士の質問です。標準的な見積もりに基づいて回答を得たいと思っています。C++ コードの書き方についてアドバイスを求めているわけではありません。私がコンパイラの作者であるかのように答えてください。
排他的なサブオブジェクト (#) のみ、特に非仮想ベース (仮想ベース クラスが 1 回だけ名前が付けられたもの) のみを持つオブジェクトの構築中に、ベース クラス サブオブジェクトを参照する左辺値の動的型が「増加」します。ベースの型から実行中のコンストラクターのクラスの型まで。
(#) サブオブジェクトは、それがちょうど 1 つの他のオブジェクト (別のサブオブジェクトまたは完全なオブジェクトである可能性があります) の直接のサブオブジェクトである場合、排他的です。メンバーと非仮想ベースは常に排他的です。
破棄中、型は減少します (そのサブオブジェクトのデストラクタの本体の最後まで、サブオブジェクトがなくなり、動的型がなくなります)。
[共有基底クラス サブオブジェクトを持つオブジェクト (つまり、少なくとも仮想ベースを持つ別個の基本サブオブジェクトを持つクラス内) の構築中に、基本サブオブジェクトの動的型が一時的に「消える」ことがあります。そのようなクラスについてここで議論したくはありません。]
問題は、オブジェクトの動的タイプが別のスレッドで増加するとどうなるかということです。
質問のタイトルは標準 C++ の質問ですが、非標準用語 (vptr)を使用して表現されているため、矛盾しているように見える場合があります。理由は次のとおりです。
- ポリモーフィズムが vptr に関して実装されている必要はありませんが、(ほとんど?) 常に実装されています。オブジェクト内の 1 つ (または複数) の vptr は、ポリモーフィック オブジェクトの動的な型を表します。
- データ競合は、メモリ位置への読み取り/書き込み操作に関して定義されます。
- 標準テキストでは、標準機能を定義するために「解説のみ」で非標準要素を使用することがよくあります。(だから、なぜ vptr を「説明のためだけに」使わないのですか?)
標準では、動的型の関数として、ポリモーフィック オブジェクト (*) の動作を直接定義していません。標準では、いわゆる「ライフタイム」(コンストラクターが完了した後) の間、最も派生した型のコンストラクターの本体内でどの式が許可されるかを指定します (まったく同じ式が同じセマンティックで許可されます)。基本クラスのサブオブジェクト コンストラクター...
(*) ポリモーフィック オブジェクトまたは動的オブジェクトの動的動作 (**) には、次のものが含まれます: 仮想呼び出し、派生からベースへの変換、ダウン キャスト (static_cast
またはdynamic_cast
)、typeid
ポリモーフィック オブジェクト。
(**) 動的オブジェクトは、そのクラスが virtual キーワードを使用するオブジェクトです。そのため、そのコンストラクターは自明ではありません。
したがって、説明は次のように述べています。何かが終了した後、何かが開始されるとすぐに、他の何かの前に、など. いくつかの式は有効であり、そのようなことを行います.
構築と破棄の仕様は、スレッドが標準 C++ の一部になる前に書かれました。では、スレッドの標準化によって何が変わったのでしょうか? スレッド動作の定義 (規範部分) [basic.life] /11を含む 1 つの文があります。
この節では、「前」と「後」は「前に起こる」関係 ([intro.multithread]) を指します。
したがって、コンストラクターの呼び出しの完了とオブジェクトの使用の間に発生前の関係があり、オブジェクトのその使用の前に発生し、デストラクタの呼び出しがある場合、オブジェクトは完全に構築されていると見なされることは明らかです。 (それがまったく呼び出された場合)。
ただし、基本クラスのサブオブジェクトが構築された後、派生クラスの構築中に何が起こるかについては述べていません。構築中のポリモーフィック オブジェクトに動的プロパティが使用されている場合、明らかに競合状態がありますが、競合状態は不正ではありません。
[競合状態は非決定論のケースであり、ミューテックス、条件変数、rwlocks、セマフォの多くの使用、他の同期デバイスの多くの使用、およびアトミックプリミティブのすべての使用の意味のある使用は、少なくともアトミック オブジェクトの変更順序のレベル。その低レベルの非決定性が予測不可能な高レベルの動作をもたらすかどうかは、プリミティブの使用方法に依存します。]
次に、標準草案は次のように続けます。
[注: したがって、あるスレッドで構築されているオブジェクトが適切な同期なしで別のスレッドから参照されると、未定義の動作が発生します。— エンドノート]
「適切な同期」はどこで定義されていますか?
「適切な同期」の欠如は、通常のデータ競合と道徳的に同等ですか: vptr でのデータ競合、または標準的に言えば、動的型でのデータ競合ですか?
簡単にするために、少なくとも最初のステップとして、質問の範囲を単一の継承に制限したいと思います。(いずれにせよ、多重継承によるオブジェクトの構築に関して、標準はひどく混乱しています。)
これは言語弁護士の質問なので、私は興味がありません:
- 別のスレッドで構築中のオブジェクトを使用することが推奨されるかどうか (おそらく推奨されません)。
- 同期を使用してその競合状態を確実に修正する方法。
- コンパイラ ベンダーがそのようなユース ケースをサポートするかどうか (おそらくしないし、しない);
- それが実際の実装で確実に機能する可能性があるかどうか(現在の実装では、重要なケースでは確実に機能しない可能性があります)。
編集:前の例は、問題を説明する代わりに、気を散らすものでした。チャットセクションで非常に興味深いが、まったく無関係な議論を引き起こしました.
同じ問題を引き起こさないよりクリーンな例を次に示します。
atomic<Base1*> shared;
struct Base1 {
virtual void f() {}
};
struct Base2 : Base1 {
virtual void f() {}
Base2 () { shared = (Base1*)this; }
};
struct Der2 : Base2 {
virtual void f() {}
};
void use_shared() {
Base1 *p;
while (! (p = shared.get()));
p->f();
}
コンシューマー/プロデューサー ロジックの場合:
- スレッド A:
new Der2;
- スレッド B:
use_shared();
参考までに、元の例:
atomic<Base*> shared;
struct Base {
virtual void f() {}
Base () { shared = this; }
};
struct Der : Base {
virtual void f() {}
};
void use_shared() {
Base *p;
while (! (p = shared.get()));
p->f();
}
コンシューマー/プロデューサーのロジック:
- スレッド A:
new Der;
- スレッド B:
use_shared();
this
コンストラクターの実行中に別のスレッドで使用できるかどうかは明確ではありませんでしたBase
。これは興味深い問題ですが、派生コンストラクターが別のスレッドで実行されている間に基本クラスのサブオブジェクトを使用するという問題とは無関係です。
追加情報
参考までに、現在の言い回しを「動機付けた」DR (それは何も説明していませんが):