C のマクロは、stddef.hにあるANSI Cライブラリ機能です。これは、構造体または共用体型内の指定されたメンバのオフセット(バイト単位) に評価され、型はsize_tの式になります。このマクロは 2 つのパラメータを受け取ります。1 つ目は構造体または共用体の名前、2 つ目は構造体/共用体のサブオブジェクトの名前で、ビット フィールドではありません。C プロトタイプとして記述することはできません。[ 1 ]offsetof()offsetof()
マクロの「従来型」実装では、コンパイラがアドレスゼロから始まる仮想的な構造体を指定することで、メンバのオフセットを取得することに依存していた。
#define offsetof(st, m) \ ((size_t)&(((st*)0)->m))これは、構造体st型のヌルポインタを取得し、その構造体内のメンバmのアドレスを取得することと理解できます。この実装は多くのコンパイラで正しく動作しますが、ヌルポインタの逆参照が含まれているように見えるため、C 標準に従って未定義の動作であるかどうかについて議論が生じています[ 2 ]。また、引数のいずれかのスペルが間違っていると、コンパイラの診断が混乱を招く傾向があります。
別の選択肢としては、以下のものがあります。
#define offsetof(st, m) \ ((size_t)((char*)&((st*)0)->m - (char*)0))規格ではヌルポインタの内部表現がアドレス0にあるとは規定されていないため、このような指定になっている可能性があります。したがって、メンバアドレスとベースアドレスの差を計算する必要があるのです。
一部の最新のコンパイラ( GCCなど)は、代わりに特別な形式(言語拡張として)を使用してマクロを定義します。例:[ 3 ]
#define offsetof(st, m) \ __builtin_offsetof(st, m)これは、C言語で汎用データ構造を実装する際に役立ちます。たとえば、Linuxカーネルは、container_of( )を実装するためにoffsetof()を使用しており、これにより、ミックスイン型のようなものが、それを含む構造を見つけることができます。[ 5 ]
#define CONTAINER_OF(ptr, Type, member) ({ \ const typeof(((Type*)0)->member) *__mptr = (ptr); \ (Type*)((char*)__mptr - offsetof(Type, member));})このマクロは、ネストされた要素へのポインタから、囲んでいる構造体を取得するために使用されます。例えば、MyStructオブジェクトのリンクリストのこの反復処理などがこれに該当します。
struct MyStruct { const char * name ; struct ListNode list ; };extern struct ListNode * listNext ( struct ListNode * );struct ListNode * current = /* ... */ ;while ( current ) { struct MyStruct * element = CONTAINER_OF ( current , struct MyStruct , list ); printf ( "%s \n " , element- > name ); current = listNext ( & element- > list ); }Linuxカーネルのcontainer_ofの実装では、ステートメント式と呼ばれるGNU C拡張機能が使用されています。[ 6 ]ステートメント式は、型安全性を確保し、潜在的な偶発的なバグを排除するために使用された可能性があります。しかし、ステートメント式を使用せずに同じ動作を実装しつつ、型安全性を確保する方法があります。
#define CONTAINER_OF(ptr, Type, member) ((Type*)((char*)(1 ? (ptr) : &((Type*)0)->member) - offsetof(Type, member)))一見すると、この実装は必要以上に複雑に思えるかもしれませんし、条件演算子の通常とは異なる使い方も不自然に感じられるかもしれません。しかし、よりシンプルな実装も可能です。
#define CONTAINER_OF(ptr, Type, member) ((Type*)((char*)(ptr) - offsetof(Type, member)))この実装も同様の目的を果たすが、オリジナルの Linux カーネル実装と比較して根本的な欠落がある。それは、の型がptrメンバーの型と照合されないことである。
前述の型チェック実装では、条件演算子の特殊な使用法によってチェックが行われます。条件演算子の制約では、条件演算子のオペランドが両方とも型へのポインタである場合、両方とも互換性のある型へのポインタでなければならないと規定されています。この場合、条件式の3番目のオペランドの値は決して使用されないにもかかわらず、コンパイラは と が(ptr)両方&((Type*)0)->memberとも互換性のあるポインタ型であることを確認するためのチェックを実行する必要があります。
の使用は、 C++98ではPOD型、C++11では標準レイアウトクラスoffsetofに限定されています。[7] C++17 では条件付きでより多くのケースがサポートされています。[ 8 ]それ以外の場合は未定義の動作となります。ほとんどのコンパイラは標準に準拠しない場合でも正しい結果を生成しますが、offsetof が誤った値を生成したり、コンパイル時の警告やエラーを生成したり、プログラムを完全にクラッシュさせたりするエッジケースがあります。これは特に仮想継承の場合に当てはまります。[ 9 ] 次のプログラムは、amd64 アーキテクチャで gcc 4.7.3 を使用してコンパイルすると、いくつかの警告を生成し、明らかに疑わしい結果を出力します。
#include <cstddef>import std ;struct Base { int a ; virtual void dummy () {} };struct Derived : virtual public Base { int b ; };int main () { std :: println ( "offsetof(Base, a) : {}" , offsetof ( Base , a )); std :: println ( "offsetof(Derived, b) : {}" , offsetof ( Derived , b )); return 0 ; }出力は以下のとおりです。
offsetof(Base, a) : 8 offsetof(Derived, b) : 8