Bluehorn の答えは正しいですが、私にとっては、問題の理由を最も簡単な言葉で説明していません。私がそれを理解する方法は次のとおりです。
NonPOD が非 POD クラスの場合、次のようになります。
NonPOD np;
np.field;
コンパイラは、ベース ポインターにオフセットを追加して逆参照することによって、必ずしもフィールドにアクセスするとは限りません。POD クラスの場合、C++ 標準はそれを行う (または同等のことを行う) ように制約しますが、非 POD クラスの場合はそうしません。代わりに、コンパイラはオブジェクトからポインターを読み取り、その値にオフセットを追加してフィールドの格納場所を指定し、逆参照する場合があります。フィールドが NonPOD の仮想ベースのメンバーである場合、これは仮想継承の一般的なメカニズムです。しかし、その場合に限定されるものではありません。コンパイラは、好きなことをほとんど何でも行うことができます。必要に応じて、コンパイラによって生成された非表示の仮想メンバー関数を呼び出すことができます。
複雑なケースでは、フィールドの位置を整数オフセットとして表すことは明らかに不可能です。そのoffsetof
ため、POD 以外のクラスでは無効です。
コンパイラがたまたま単純な方法でオブジェクトを保存する場合(単一継承、通常は非仮想多重継承、通常はオブジェクトを参照しているクラスで定義されたフィールドではなく)いくつかの基本クラスで)、それはたまたまうまくいくでしょう。おそらく、存在するすべてのコンパイラでたまたまうまくいく場合があります。これでは有効になりません。
付録: 仮想継承はどのように機能しますか?
単純な継承では、B が A から派生している場合、通常の実装では、B へのポインターは A へのポインターにすぎず、B の追加データが最後にスタックされます。
A* ---> field of A <--- B*
field of A
field of B
単純な多重継承では、通常、B の基底クラス (A1 および A2 と呼びます) が B に特有の順序で配置されていると想定します。しかし、ポインターを使用した同じトリックは機能しません。
A1* ---> field of A1
field of A1
A2* ---> field of A2
field of A2
A1 と A2 は、どちらも B の基本クラスであるという事実を「認識」していません。したがって、B* を A1* にキャストする場合は、A1 のフィールドを指す必要があり、A2* にキャストする場合は、それを指す必要があります。 A2 のフィールドを指す必要があります。ポインター変換演算子はオフセットを適用します。したがって、次のようになる可能性があります。
A1* ---> field of A1 <---- B*
field of A1
A2* ---> field of A2
field of A2
field of B
field of B
次に、B* を A1* にキャストしてもポインター値は変更されませんが、A2* にキャストするとsizeof(A1)
バイトが追加されます。これが、仮想デストラクタがない場合に A2 へのポインタを介して B を削除するとうまくいかない「その他の」理由です。B と A1 のデストラクタの呼び出しに失敗するだけでなく、正しいアドレスを解放することさえできません。
とにかく、B はすべての基本クラスがどこにあるかを「知って」おり、常に同じオフセットに格納されています。したがって、この配置でも offsetof は引き続き機能します。標準では、実装がこのように多重継承を行う必要はありませんが、多くの場合 (またはそれに類する) 必要があります。したがって、offsetof はこの場合、実装で機能する可能性がありますが、保証されていません。
では、仮想継承はどうでしょうか。B1 と B2 の両方が仮想ベースとして A を持っているとします。これにより、それらは単一継承クラスになるため、最初のトリックが再び機能すると考えるかもしれません。
A* ---> field of A <--- B1* A* ---> field of A <--- B2*
field of A field of A
field of B1 field of B2
しかし、ちょっと待ってください。C が B1 と B2 の両方から (簡単にするために非仮想的に) 派生するとどうなるでしょうか? C には、A のフィールドのコピーが 1 つだけ含まれている必要があります。これらのフィールドは、B1 のフィールドの直前に置くことはできず、B2 のフィールドの直前にも置くことはできません。困っています。
したがって、代わりに実装が行う可能性があるのは次のとおりです。
// an instance of B1 looks like this, and B2 similar
A* ---> field of A
field of A
B1* ---> pointer to A
field of B1
A サブオブジェクトの後のオブジェクトの最初の部分を指している B1* を示しましたが、実際のアドレスはそこにないのではないかと思います (わざわざ確認する必要はありません)。A の先頭になります。単純な継承、つまりポインターの実際のアドレスと図で示したアドレスの間のオフセットは、コンパイラーがオブジェクトの動的な型を特定しない限り、決して使用されません。代わりに、常にメタ情報を経由して A に正しく到達します。そのオフセットは、関心のある用途に常に適用されるため、私の図はそこを指します。
A への「ポインター」は、ポインターまたはオフセットである可能性がありますが、実際には問題ではありません。B1 として作成された B1 のインスタンスでは、それは を指し(char*)this - sizeof(A)
、B2 のインスタンスでも同じです。しかし、C を作成すると、次のようになります。
A* ---> field of A
field of A
B1* ---> pointer to A // points to (char*)(this) - sizeof(A) as before
field of B1
B2* ---> pointer to A // points to (char*)(this) - sizeof(A) - sizeof(B1)
field of B2
C* ----> pointer to A // points to (char*)(this) - sizeof(A) - sizeof(B1) - sizeof(B2)
field of C
field of C
したがって、B2 へのポインターまたは参照を使用して A のフィールドにアクセスするには、オフセットを適用するだけでは不十分です。B2 の「A へのポインター」フィールドを読み取り、それに従って、オフセットを適用する必要があります。これは、B2 がどのクラスのベースであるかに応じて、そのポインターの値が異なるためです。というようなものはありoffsetof(B2,field of A)
ません:あり得ません。どの実装でも、 offsetof は仮想継承では機能しません。