52

C++ でメンバーのメモリ オフセットをクラスに取得する方法を調査していたところ、ウィキペディアで次のような情報を見つけました。

C++ コードでは、offsetof を使用して、プレーン オールド データ構造体ではない構造体またはクラスのメンバーにアクセスすることはできません。

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

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 データ構造とは何かを誤解しているか、パズルの他のピースが欠けていると思います。問題が何であるかわかりません。

4

11 に答える 11

48

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 は仮想継承では機能しません

于 2009-07-15T11:03:48.827 に答える
37

簡単な答え: 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静的に見つけようとするとクラッシュします。これは、基本クラスの場所がルックアップ テーブルに格納される仮想継承によるものです。

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

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

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

于 2009-07-15T08:03:52.480 に答える
5

In general, when you ask "why is something undefined", the answer is "because the standard says so". Usually, the rational is along one or more reasons like:

  • it is difficult to detect statically in which case you are.

  • corner cases are difficult to define and nobody took the pain of defining special cases;

  • its use is mostly covered by other features;

  • existing practices at the time of standardization varied and breaking existing implementation and programs depending on them was deemed more harmful that standardization.

Back to offsetof, the second reason is probably a dominant one. If you look at C++0X, where the standard was previously using POD, it is now using "standard layout", "layout compatible", "POD" allowing more refined cases. And offsetof now needs "standard layout" classes, which are the cases where the committee didn't want to force a layout.

You have also to consider the common use of offsetof(), which is to get the value of a field when you have a void* pointer to the object. Multiple inheritance -- virtual or not -- is problematic for that use.

于 2009-07-15T10:14:03.480 に答える
2

あなたのクラスは 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 つだけ存在する場合があります。
于 2009-07-15T08:50:28.547 に答える
0

PODデータ構造の定義については、ここで説明します[スタックオーバーフローの別の投稿に既に投稿されています]

C++ の POD 型とは何ですか?

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

上記の私の見解があなたの疑問を明確にしない場合、正しい質問を教えてください。

于 2009-07-15T07:30:06.900 に答える
-3

たとえば、仮想の空のデストラクタを追加する場合:

virtual ~Foo() {}

クラスは「多態的」になります。つまり、仮想関数へのポインタを含む「vtable」へのポインタである非表示のメンバ フィールドを持つことになります。

非表示のメンバー フィールドのため、オブジェクトのサイズとメンバーのオフセットは簡単ではありません。したがって、offsetof を使用すると問題が発生するはずです。

于 2009-07-15T07:34:16.207 に答える
-3

これを VC++ でコンパイルしてください。g++ で試してみて、どのように動作するかを確認してください...

簡単に言えば、未定義ですが、一部のコンパイラでは許可されている場合があります。他の人はしません。とにかく携帯性が悪い。

于 2009-07-15T07:37:41.160 に答える
-4

C++ では、次のように相対オフセットを取得できます。

class A {
public:
  int i;
};

class B : public A {
public:
  int i;
};

void test()
{
  printf("%p, %p\n", &A::i, &B::i); // edit: changed %x to %p
}
于 2009-07-15T07:35:10.807 に答える