web-dev-qa-db-ja.com

C ++の非POD構造でoffsetofを使用できないのはなぜですか?

メンバーのメモリオフセットをC++のクラスに取得する方法を調査していて、これに遭遇しました wikipedia:

C++コードでは、offsetofを使用して、Plain Old Data構造ではない構造またはクラスのメンバーにアクセスすることはできません。

私はそれを試しました、そしてそれはうまくいくようです。

class Foo
{
private:
    int z;
    int func() {cout << "this is just filler" << endl; return 0;}

public: 
    int x;
    int y;
    Foo* f;

    bool returnTrue() { return false; }
};

int main()
{
    cout << offsetof(Foo, x)  << " " << offsetof(Foo, y) << " " << offsetof(Foo, f);
    return 0;
}

警告がいくつかありましたが、コンパイルされ、実行すると妥当な出力が得られました。

Laptop:test alex$ ./test
4 8 12

PODデータ構造とは何かを誤解しているのか、パズルの他のピースが欠けているのかと思います。何が問題なのかわかりません。

49
Alex

短い答え:offsetofは、レガシーC互換性のためのC++標準のみにある機能です。したがって、基本的にはCで実行できるものに限定されます。C++は、Cの互換性のために必要なものだけをサポートします。

Offsetofは基本的にCをサポートする単純なメモリモデルに依存するハック(マクロとして実装)であるため、C++コンパイラの実装者がクラスインスタンスレイアウトを整理する方法から多くの自由を奪います。

その効果は、offsetofがC++で(使用されているソースコードとコンパイラーに応じて)多くの場合、標準に裏付けられていない場合でも(そうでない場合を除いて)機能することです。したがって、C++でのoffsetofの使用には特に注意が必要です。特に pOD以外の使用に対して警告を生成する単一のコンパイラーがわからないので... 最新のGCCとClangは、offsetofが標準(-Winvalid-offsetof)。

編集する:たとえば、あなたが尋ねたように、以下は問題を明らかにするかもしれません:

#include <iostream>
using namespace std;

struct A { int a; };
struct B : public virtual A   { int b; };
struct C : public virtual A   { int c; };
struct D : public B, public C { int d; };

#define offset_d(i,f)    (long(&(i)->f) - long(i))
#define offset_s(t,f)    offset_d((t*)1000, f)

#define dyn(inst,field) {\
    cout << "Dynamic offset of " #field " in " #inst ": "; \
    cout << offset_d(&i##inst, field) << endl; }

#define stat(type,field) {\
    cout << "Static offset of " #field " in " #type ": "; \
    cout.flush(); \
    cout << offset_s(type, field) << endl; }

int main() {
    A iA; B iB; C iC; D iD;
    dyn(A, a); dyn(B, a); dyn(C, a); dyn(D, a);
    stat(A, a); stat(B, a); stat(C, a); stat(D, a);
    return 0;
}

これは、インスタンスa内のフィールドBを静的に検索しようとするとクラッシュしますが、インスタンスが使用可能な場合は機能します。これは、基本クラスの場所がルックアップテーブルに格納される仮想継承によるものです。

これは不自然な例ですが、実装ではルックアップテーブルを使用して、クラスインスタンスのパブリック、保護、プライベートセクションを見つけることもできます。または、ルックアップを完全に動的にする(フィールドにハッシュテーブルを使用する)など。

標準では、offsetofをPODに制限することですべての可能性を開いたままにします(IOW:POD構造体にハッシュテーブルを使用する方法はありません... :)

もう1つの注意:仮想ベースクラスのフィールドに対してoffsetofを呼び出すとGCCが実際にエラーになるため、この例ではoffsetof(ここではoffset_s)を再実装する必要がありました。

36
Bluehorn

ブルーホーンの答えは正しいですが、私にとっては、問題の理由を最も簡単な言葉で説明していません。私がそれを理解する方法は次のとおりです:

NonPODが非PODクラスである場合は、次のようにします。

_NonPOD np;
np.field;
_

コンパイラーは、ベースポインターにオフセットを追加して逆参照することで、必ずしもフィールドにアクセスするわけではありません。 PODクラスの場合、C++標準はそれを行うように(または同等のことを)制限しますが、非PODクラスの場合はしません。代わりに、コンパイラはオブジェクトからポインタを読み取り、オフセットをthat値に追加してフィールドの格納場所を指定し、逆参照する場合があります。これは、フィールドがNonPODの仮想ベースのメンバーである場合、仮想継承の一般的なメカニズムです。しかし、それはその場合に限定されません。コンパイラーは、好きなことはほとんど何でもできます。必要に応じて、コンパイラが生成した非表示の仮想メンバー関数を呼び出すことができます。

複雑なケースでは、フィールドの位置を整数オフセットとして表すことは明らかに不可能です。したがって、offsetofは非PODクラスでは無効です。

コンパイラーがたまたまオブジェクトを単純な方法で保存した場合(単一の継承、通常は非仮想の複数の継承など、通常はクラスとは対照的に、オブジェクトを参照しているクラスで直接定義されているフィールド)一部の基本クラスでは)、それでうまくいくでしょう。たまたまあるすべてのコンパイラでたまたま動作するケースがあるでしょう。これはそれを有効にしません。

付録:仮想継承はどのように機能しますか?

単純な継承では、BがAから派生している場合、通常の実装では、Bへのポインターは単にAへのポインターであり、Bの追加データは末尾にスタックされています。

_A* ---> field of A  <--- B*
        field of A
        field of B
_

単純な多重継承では、通常、Bの基本クラス( 'em 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
_

私はB1 *がAサブオブジェクトの後のオブジェクトの最初の部分を指していることを示しましたが、実際のアドレスはそこにないはずです(わざわざ確認することなく)、それは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はneverを実装し、任意の実装で仮想継承を使用します。

43
Steve Jessop

一般に、「なぜ未定義なのか」と尋ねると、答えは「標準でそう言われているため」となります。通常、有理数は次のような1つ以上の理由に沿っています。

  • その場合、静的に検出することは困難です。

  • コーナーケースを定義するのは難しく、特別なケースを定義するのに苦労した人はいません。

  • その使用は主に他の機能によってカバーされています。

  • 標準化の時点での既存の慣行は多様であり、それらに依存する既存の実装とプログラムを壊すことは、その標準化よりも有害であると考えられました。

Offsetofに戻ると、2番目の理由はおそらく支配的なものです。標準が以前PODを使用していたC++ 0Xを見ると、「標準レイアウト」、「レイアウト互換」、「POD」を使用しているため、より洗練されたケースが可能です。そして、offsetofは「標準レイアウト」クラスを必要とします。これは、委員会がレイアウトを強制したくない場合です。

オブジェクトへのvoid *ポインターがある場合にフィールドの値を取得するという、offsetof()の一般的な使用法も考慮する必要があります。多重継承-仮想かどうかにかかわらず、その使用には問題があります。

6
AProgrammer

あなたのクラスはPODのc ++ 0x定義に適合していると思います。 g ++は最新リリースでc ++ 0xの一部を実装しています。 VS2008にもc ++ 0xビットが含まれていると思います。

から ウィキペディアのc ++ 0x記事

C++ 0xは、POD定義に関するいくつかのルールを緩和します。

クラス/構造体が自明で標準的なレイアウトであり、静的でないメンバーがすべてPODである場合、PODと見なされます。

自明なクラスまたは構造体は、次のいずれかとして定義されます。

  1. 簡単なデフォルトのコンストラクタがあります。これは、デフォルトのコンストラクタ構文(SomeConstructor()= default;)を使用できます。
  2. 単純なコピーコンストラクターがあり、デフォルトの構文を使用できます。
  3. 簡単なコピー代入演算子があり、デフォルトの構文を使用できます。
  4. 取るに足らないデストラクタがあり、それは仮想であってはなりません。

標準レイアウトクラスまたは構造体は、次のいずれかとして定義されます。

  1. 標準レイアウトタイプの非静的データメンバーのみがあります
  2. すべての非静的メンバーに対して同じアクセス制御(パブリック、プライベート、保護)を持っている
  3. 仮想機能はありません
  4. 仮想基本クラスはありません
  5. 標準レイアウトタイプの基本クラスのみがあります
  6. 最初に定義された非静的メンバーと同じタイプの基本クラスがない
  7. 非静的メンバーを持つ基本クラスがないか、最も派生したクラスに非静的データメンバーがなく、非静的メンバーを持つ最大1つの基本クラスがあります。本質的に、このクラスの階層には、静的でないメンバーを持つクラスが1つだけ存在する場合があります。
2
KitsuneYMG

PODデータ構造の定義については、ここで説明します[Stack Overflowの別の投稿に既に投稿されています]

C++のPODタイプとは

今、あなたのコードに来ると、それは期待通りにうまく機能しています。これは、有効なクラスのパブリックメンバーのoffsetof()を見つけようとしているためです。

上記の私の見解で疑問が明確にされない場合は、正しい質問でお知らせください。

1
Roopesh Majeti

これは常に機能し、CとC++の両方で使用される最も移植性の高いバージョンです

#define offset_start(s) s
#define offset_end(e) e
#define relative_offset(obj, start, end) ((int64_t)&obj->offset_end(end)-(int64_t)&obj->offset_start(start))

struct Test {
     int a;
     double b;
     Test* c;
     long d;
 }


int main() {
    Test t;
    cout << "a " << relative_offset((&t), a, a) << endl;
    cout << "b " << relative_offset((&t), a, b) << endl;
    cout << "c " << relative_offset((&t), a, c) << endl;
    cout << "d " << relative_offset((&t), a, d) << endl;
    return 0;
}

上記のコードでは、構造体またはクラスのいずれかのオブジェクトのインスタンスを保持する必要があるだけです。次に、クラスまたは構造体へのポインタ参照を渡して、そのフィールドにアクセスする必要があります。正しいオフセットを得るために、「開始」フィールドを「終了」フィールドの下に設定しないでください。コンパイラを使用して、実行時のアドレスオフセットを特定します。

これにより、コンパイラのパディングデータなどの問題を心配する必要がなくなります。

0