C++には標準レイアウトクラス (standard-layout class) という概念があります。これがなかなかの曲者で、定義が分かりにくい上に、標準レイアウトクラスだと何ができるのか(何が嬉しいのか)も簡単ではありません。
おまけに、多くのコンパイラで標準レイアウトクラスの判定にバグが存在しており、しまいには規格書の記述にすら矛盾が存在するように見えます。
この記事では、規格書を読んで標準レイアウトクラスの定義を確認しながら各コンパイラの挙動を確かめていきます。そして私の読解力では規格書のバグに思える記述について、あわよくば有識者の方に意見をいただけないかと期待しております。
C++の規格については、C++17のドラフトであるN4659を参照します。
テストするコンパイラ
gccとclangに関しては、Wandboxを利用させていただきました。
コンパイルオプションは基本的に -Wall -Wextra -std=c++17 -pedantic
を用います。
記事中で gcc HEAD または clang HEAD とある場合、2018/08/06時点でのWandbox環境に準じます。
- それぞれ gcc HEAD 9.0.0 20180804 と clang HEAD 8.0.0 です。
MSVCに関しては、Visual Studio 2017 Update 7 におけるv141を使用し、 /std:c++17
、/permissive-
、 /W4
を指定しています。
本記事においてテストを行う際は、gcc 8.2.0, gcc HEAD, clang 6.0.1, clang HEAD, MSVC を用います。
「テストした全てのコンパイラで正しくコンパイルが通る」と記述されていた場合、上記5つのコンパイラでコンパイルが正常に終了することを意味します。
雰囲気を掴む
規格書を参照する前に、標準レイアウトクラスの雰囲気を掴むためのふわっとした説明をします。既に標準レイアウトクラスについて知っているという人や、不正確な記述が嫌いな人は飛ばしてください。
標準レイアウトクラスは、簡単に言うと「データメンバの配置が標準的なクラス」です。もっというと、「データメンバが宣言順に並んでいるだけで、その他に余計なデータを持たない単純なクラス」のことです。
struct A
{
int a;
double b;
char c;
};
例えばこのようなクラス$\mathrm{A}$は、データメンバ$\mathrm{a}$, $\mathrm{b}$, $\mathrm{c}$が順に並んでいるだけで、他に余計なデータを何も持たないため、標準レイアウトクラスになります。
struct B
{
int a;
virtual void f() {}
};
クラス$\mathrm{B}$のように仮想関数を持つクラスは、状態量として仮想関数テーブルが必要になるため、標準レイアウトクラスではありません。
- なお仮想関数でないメンバ関数やstaticデータメンバは、クラス型オブジェクトのメモリ配置に影響を与えないため、標準レイアウトクラスの条件には無関係です。
struct C
{
int a;
};
struct D : public C
{
double b;
};
継承をしてしまうと、「データメンバが宣言順に並んでいるだけ」ではなくなってしまうため、クラス$\mathrm{D}$は標準レイアウトクラスではありません。ただし、
struct E {};
struct F : public E
{
int a;
double b;
};
このように基底クラス$\mathrm{E}$がデータメンバを持たない場合、$\mathrm{F}$のメモリ配置はデータメンバ$\mathrm{a}$, $\mathrm{b}$が順に並んでいるだけになるので、$\mathrm{F}$は標準レイアウトクラスです。
- この場合、空の基底クラスはサイズが0になります。Empty Base Optimization (EBO) などで検索すると、関連する記事が出てくると思います。
注意点として、標準レイアウトクラスはデータメンバのアライメント要件を満たすためにパディングを挿入することはあります。ただし、最初のデータメンバの前にパディングが無いことは保証されています。
標準レイアウトクラスの定義
標準レイアウトクラスについて確認する前に、標準レイアウト型 (standard-layout types) の定義を見てみましょう。
§6.9 [basic.types] より
Scalar types, standard-layout class types (Clause 12), arrays of such types and cv-qualified versions of these types are collectively called standard-layout types.
つまり、標準レイアウト型とは、スカラ型、標準レイアウトクラス型、それらの配列及びCV修飾された型の集合です。
スカラ型の定義は
§6.9 [basic.types] より
Arithmetic types (6.9.1), enumeration types, pointer types, pointer to member types (6.9.2), std::nullptr_t, and cv-qualified (6.9.3) versions of these types are collectively called scalar types.
です。こちらに関しては本記事では深追いしません。
本記事の中心は標準レイアウトクラスです。こちらの定義は
§12 [class] より
A class $\mathrm{S}$ is a standard-layout class if it:
- has no non-static data members of type non-standard-layout class (or array of such types) or reference,
- has no virtual functions (13.3) and no virtual base classes (13.1),
- has the same access control (Clause 14) for all non-static data members,
- has no non-standard-layout base classes,
- has at most one base class subobject of any given type,
- has all non-static data members and bit-fields in the class and its base classes first declared in the same class, and
- has no element of the set $M(\mathrm{S})$ of types (defined below) as a base class.
$M(\mathrm{X})$ is defined as follows:
- If $\mathrm{X}$ is a non-union class type with no (possibly inherited (Clause 13)) non-static data members, the set $M(\mathrm{X})$ is empty.
- If $\mathrm{X}$ is a non-union class type whose first non-static data member has type $\mathrm{X_0}$ (where said member may be an anonymous union), the set $M(\mathrm{X})$ consists of $\mathrm{X_0}$ and the elements of $M(\mathrm{X_0})$.
- If $\mathrm{X}$ is a union type, the set $M(X)$ is the union of all $M(\mathrm{U_i})$ and the set containing all $\mathrm{U_i}$, where each $\mathrm{U_i}$ is the type of the $i$th non-static data member of $\mathrm{X}$.
- If $\mathrm{X}$ is an array type with element type $\mathrm{X_e}$, the set $M(\mathrm{X})$ consists of $\mathrm{X_e}$ and the elements of $M(\mathrm{X_e})$.
- If $\mathrm{X}$ is a non-class, non-array type, the set $M(\mathrm{X})$ is empty.
[ Note: $M(\mathrm{X})$ is the set of the types of all non-base-class subobjects that are guaranteed in a standard-layout class to be at a zero offset in $\mathrm{X}$. — end note ]
となっています。各条件については後ほど細かく確認します。
最後に気になるNoteが書いてあります。このNoteだけではわかりにくいですが、標準レイアウトクラスの最初のデータメンバは、オフセットがゼロになることが保証されているのです。つまり標準レイアウト型オブジェクトのアドレスと、その最初のデータメンバのアドレスは等しくなります。
§12.2 [class.mem] より
If a standard-layout class object has any non-static data members, its address is the same as the address of its first non-static data member. Otherwise, its address is the same as the address of its first base class subobject (if any).
[ Note: There might therefore be unnamed padding within a standard-layout struct object, but not at its beginning, as necessary to achieve appropriate alignment. - end note ]
[ Note: The object and its first subobject are pointer-interconvertible (6.9.2, 8.2.9). - end note ]
条件1.1 標準レイアウトでないクラス型の非staticデータメンバを持たない
has no non-static data members of type non-standard-layout class (or array of such types) or reference,
の前半部分です。妥当な条件ですね。
条件1.2 参照の非staticデータメンバを持たない
has no non-static data members of type non-standard-layout class (or array of such types) or reference,
の後半部分です。参照の表現方法については規格で細かく指定されていないため、参照を含むクラスが標準レイアウトクラスにならないのは納得です。
#include <type_traits>
struct S
{
int& a;
};
static_assert(!std::is_standard_layout_v<S>);
このコードは、テストした全てのコンパイラで正しくコンパイルが通りました。
条件2 仮想関数や仮想基底クラスを持たない
has no virtual functions (13.3) and no virtual base classes (13.1),
仮想関数を持つクラスは仮想関数テーブルを状態量として持つため、標準レイアウトクラスになりません。
#include <type_traits>
struct S
{
virtual void f() {}
};
static_assert(!std::is_standard_layout_v<S>);
このコードは、テストした全てのコンパイラで正しくコンパイルが通りました。
条件3 全ての非staticデータメンバが同じアクセス指定を持つ
has the same access control (Clause 14) for all non-static data members,
アクセス指定が異なるデータメンバは、宣言順に並ぶことが保証されていません。
§12.2 [class.mem] より
Non-static data members of a (non-union) class with the same access control (Clause 14) are allocated so that later members have higher addresses within a class object. The order of allocation of non-static data members with different access control is unspecified (Clause 14).
#include <type_traits>
struct S
{
public:
int a;
private:
int b;
};
static_assert(!std::is_standard_layout_v<S>);
このコードは、テストした全てのコンパイラで正しくコンパイルが通りました。
条件4 非標準レイアウトな基底クラスを持たない
has no non-standard-layout base classes,
これも妥当な条件ですね。
条件5 あらゆる型Xに対して、X型の基底クラスサブオブジェクトをたかだか一つしか持たない。
has at most one base class subobject of any given type,
この条件を理解するのはやや難しく、少し事前知識が必要です。
§4.5 [intro.object] より
Unless it is a bit-field (12.2.4), a most derived object shall have a nonzero size and shall occupy one or more bytes of storage. Base class subobjects may have zero size.
C++ではオブジェクトは基本的に非ゼロのサイズを持ちますが、基底クラスサブオブジェクトはサイズがゼロであることを許されています。
§4.5 [intro.object] より
Two objects $a$ and $b$ with overlapping lifetimes that are not bit-fields may have the same address if one is nested within the other, or if at least one is a base class subobject of zero size and they are of different types; otherwise, they have distinct addresses.
基本的に二つのオブジェクトは異なるアドレスを持ちますが、これには二つの例外があります。
一つはサブオブジェクトに関するもので、$\mathrm{a}$が$\mathrm{b}$のデータメンバや基底クラスサブオブジェクトの場合などです。
#include <iostream>
struct X
{
int i;
};
struct Y
{
X x;
};
int main()
{
Y a;
X& b = a.x;
std::cout << &a << std::endl;
std::cout << &b << std::endl;
// 同じアドレスが表示される。
return 0;
}
もう一つは、どちらかがサイズ0の基底クラスサブオブジェクトの場合です。
#include <iostream>
struct X {};
struct Y : X
{
int i;
};
int main()
{
Y y;
X& a = static_cast<X&>(y);
int& b = y.i;
std::cout << &a << std::endl;
std::cout << &b << std::endl;
// 同じアドレスが表示される。
return 0;
}
このコードでは、$\mathrm{a}$と$\mathrm{b}$はどちらかがどちらかのサブオブジェクトではないのですが、同じアドレスを持ちます。
- $\mathrm{a}$(の参照先)も$\mathrm{b}$(の参照先)もyのサブオブジェクトです。
-
static_cast<A&>
は無くても問題ありませんが、$\mathrm{a}$(の参照先)が$\mathrm{y}$のサブオブジェクトであることをわかりやすくするためにあえて記述しています。
これは、$\mathrm{a}$の参照先がサイズ0の基底クラスサブオブジェクトだからです。
ただし、$\mathrm{a}$と$\mathrm{b}$が同じ型の場合は、必ず異なるアドレスを持ちます。
#include <iostream>
struct X {};
struct Y : X
{
X x;
};
int main()
{
Y y;
X& a = static_cast<X&>(y);
X& b = y.x;
std::cout << &a << std::endl;
std::cout << &b << std::endl;
// 必ず異なるアドレスが表示される。
return 0;
}
この条件により、たとえ基底クラスがデータメンバを全く持たなくても、サイズをゼロにすることができなくなる場合があります。
- 上記の例もその一つです。
- 空の基底クラスサブオブジェクトがサイズを持つと考える代わりに、非ゼロのパディングが挿入されると考えることもできます。
これは、基底クラスと最初のデータメンバが同じ型であるときの他に、以下のような場合が考えられます。
#include <iostream>
struct A {};
struct B : A {};
struct C {};
struct D : B, C
{
int x;
};
struct E : A {};
struct F : B, E
{
int y;
};
int main()
{
{
D d;
B& b = static_cast<B&>(d);
A& a = static_cast<A&>(b);
C& c = static_cast<C&>(d);
std::cout << &a << std::endl;
std::cout << &b << std::endl;
std::cout << &c << std::endl;
std::cout << &d << std::endl;
// 全て同じアドレスが表示される
}
{
F f;
B& b = static_cast<B&>(f);
A& a1 = static_cast<A&>(b);
E& e = static_cast<E&>(f);
A& a2 = static_cast<A&>(e);
std::cout << &a1 << std::endl;
std::cout << &a2 << std::endl;
std::cout << &b << std::endl;
std::cout << &e << std::endl;
std::cout << &f << std::endl;
// a1とb, a2とeはそれぞれ同じアドレスを持つが、
// bとeは異なるアドレスを持つ
// これは、a1とa2が同じ型であり、
// a1とa2は異なるアドレスを持つ必要があるからである
}
return 0;
}
$\mathrm{F}$の基底クラスは全て空ですが、同じ型のサブオブジェクトを複数含むため、それらに異なるアドレスを振らなければなりません。
このような場合、どのようにアドレスを調整されるかは規格で定まっていないため、$\mathrm{F}$のようなクラスを標準レイアウトクラスにすることはできません。
つまり、継承ツリーの中に同じ型が複数回現れるようなクラスは標準レイアウトクラスではないということです。
#include <type_traits>
struct A {};
struct B : A {};
struct C : A {};
struct S : B, C {};
static_assert(!std::is_standard_layout_v<S>);
このコードは残念ながら gcc 8.2.0, gcc HEAD, clang 6.0.1 でコンパイルが通りませんでした。clang HEAD, MSVC では正しくコンパイルされました。
- $\mathrm{S}$にint型のデータメンバをもたせた場合も同様の結果となりました。
条件6 クラスS及びSの全ての基底クラスに含まれる、全ての非staticデータメンバ及びビットフィールドは、同一のクラス内で最初に宣言される。
has all non-static data members and bit-fields in the class and its base classes first declared in the same class,
継承ツリーの中でデータメンバを持つことができるのは一つのクラスだけだということです。
#include <type_traits>
struct A
{
int a;
};
struct S1 : A
{
int x;
};
static_assert(!std::is_standard_layout_v<S1>);
struct B
{
int b;
};
struct S2 : A, B {};
static_assert(!std::is_standard_layout_v<S2>);
struct C : A {};
struct D {};
struct S3 : C, D {};
static_assert(std::is_standard_layout_v<S3>);
このソースコードはテストした全てのコンパイラで正しくコンパイルされました。
$\mathrm{S1}$は、継承している$\mathrm{A}$と$\mathrm{S1}$自身の双方がデータメンバを持つため、標準レイアウトクラスではありません。
$\mathrm{S2}$は、継承している$\mathrm{A}$と$\mathrm{B}$がデータメンバを持つため、標準レイアウトクラスではありません。
$\mathrm{S3}$は、$\mathrm{D}$と$\mathrm{C}$を継承しており、$\mathrm{C}$は$\mathrm{A}$を継承していますが、$\mathrm{D}$自身を含め、データメンバを持つのが$\mathrm{A}$のみであるため、標準レイアウトクラスです。
条件7 M(S)に含まれる型を基底クラスとして持たない
has no element of the set $M(\mathrm{S})$ of types (defined below) as a base class.
この条件は、前述の「型$\mathrm{S}$の基底クラスと型$\mathrm{S}$の最初のデータメンバが同じ型であるとき、それらのアドレスを異なるものにする必要がある」「そのような場合、型$\mathrm{S}$は標準レイアウトではなくなる」という条件を厳密に記述したものです。
型$\mathrm{S}$が標準レイアウトでなくなる根拠の一つとして、最初のデータメンバのオフセットをゼロにすることができない、という点がNoteで触れられていました。
$M(\mathrm{X})$の定義を見てみましょう。
If $\mathrm{X}$ is a non-union class type with no (possibly inherited (Clause 13)) non-static data members, the set $M(\mathrm{X})$ is empty.
$\mathrm{X}$が非unionで非staticデータメンバを持たない場合、$M(\mathrm{X})$は空になります。つまり、$\mathrm{S}$がデータメンバを持たない場合、どのような基底クラスを継承していたとしても、少なくとも条件7は満たします。
「最初のデータメンバと基底クラスが同一の型になることを避ける」という目的を考えると、そもそもデータメンバを持たない場合は基底クラスに制限をかける必要はないため、妥当な条件ですね。
ただし、ここでいうデータメンバは「基底クラスから継承されたメンバ」を含むため、非staticデータメンバを持つクラスを継承している場合はそもそもこの条件に合致しません。
If $\mathrm{X}$ is a non-union class type whose first non-static data member has type $\mathrm{X_0}$ (where said member may be an anonymous union), the set $M(\mathrm{X})$ consists of $\mathrm{X_0}$ and the elements of $M(\mathrm{X_0})$.
$\mathrm{X}$がデータメンバを持つクラスの場合、$M(\mathrm{X})$は最初のデータメンバの型$\mathrm{X_0}$と、$M(\mathrm{X_0})$の全ての要素からなる集合です。
$\mathrm{X_0}$が$M(\mathrm{X})$に含まれることはわかりやすいですね。$\mathrm{X_0}$を基底クラスに持つ場合はまさに「最初のデータメンバと基底クラスが同一の型を持つ」場合です。
#include <type_traits>
struct A {};
struct S : A
{
A a;
};
static_assert(!std::is_standard_layout_v<S>);
このソースコードはテストした全てのコンパイラで正常にコンパイルされました。
$\mathrm{S}$にとっての最初のデータメンバは$\mathrm{a}$であり、その型である$\mathrm{A}$が$\mathrm{X_0}$に当たります。$\mathrm{A}$は$M(\mathrm{S})$に含まれることになりますが、$\mathrm{S}$は$\mathrm{A}$を継承しているので、条件7に違反し、結果として$\mathrm{S}$は標準レイアウトクラスではなくなります。
ここで面白いのは、$M(\mathrm{X_0})$が再帰的に$M(\mathrm{X})$に加えられる点です。
すなわち、「最初のデータメンバの最初のデータメンバ」の型も$M(\mathrm{X})$に含まれます。
#include <type_traits>
#include <iostream>
struct A {};
struct B
{
A a;
};
struct S : A
{
B b;
};
int main()
{
S s;
A& a1 = static_cast<A&>(s);
B& b = s.b;
A& a2 = b.a;
std::cout << &a1 << std::endl;
std::cout << &a2 << std::endl;
std::cout << &b << std::endl;
std::cout << &s << std::endl;
// a1とa2に異なるアドレスを割り当てなければならないため、
// 必然的にsとbのアドレスは異なる
// すなわち、Sのオブジェクトは最初のデータメンバと異なるアドレスを持つ
// よって、Sは標準レイアウトクラスではない
std::cout << std::boolalpha << std::is_standard_layout_v<S> << std::endl;
return 0;
}
ソースコード中のコメントに記述したように、「最初のデータメンバの最初のデータメンバ」の型を継承してしまうと、最初のデータメンバのオフセットをゼロにすることができなくなってしまうため、「最初のデータメンバの最初のデータメンバ」の型は$M(\mathrm{S})$に加えなければなりません。
上記のソースコードの実行結果は、規格に則るならば「$\mathrm{a1}$と$\mathrm{a2}$のアドレスが異なる」と「$\mathrm{S}$は標準レイアウトクラスではない」を満たさなければなりません。
gcc 8.2.0, gcc HEAD, clang 6.0.1 は、前者の条件は満たしましたが後者の条件は満たさず、$\mathrm{S}$を標準レイアウトクラスであると判定してしまいました。
MSVCは、後者の条件は満たしましたが前者の条件を満たさず、全て同じアドレスを出力してしまいました。
clang HEAD は双方の条件を正しく満たしました。
If $\mathrm{X}$ is a union type, the set $M(X)$ is the union of all $M(\mathrm{U_i})$ and the set containing all $\mathrm{U_i}$, where each $\mathrm{U_i}$ is the type of the $i$th non-static data member of $\mathrm{X}$.
共用体はメモリ配置の都合上、全てのデータメンバが「最初のデータメンバ」であるので、全てのデータメンバの型$\mathrm{U_i}$を$M(\mathrm{X})$に追加し、また$M(\mathrm{U_i})$に含まれる型も再帰的に$M(\mathrm{X})$に追加します。
If $\mathrm{X}$ is an array type with element type $\mathrm{X_e}$, the set $M(\mathrm{X})$ consists of $\mathrm{X_e}$ and the elements of $M(\mathrm{X_e})$.
配列の場合、最初のデータメンバに相当するものは配列の最初の要素になります。配列の最初の要素の型というのはすなわち配列の要素の型$\mathrm{X_e}$なので、$\mathrm{X_e}$及び$M(\mathrm{X_e})$に含まれる型を$\mathrm{X}$に追加します。
If $\mathrm{X}$ is a non-class, non-array type, the set $M(\mathrm{X})$ is empty.
$\mathrm{X}$がスカラ型である場合など、クラスや配列ではない場合、$M(\mathrm{X})$は空になります。それらの型はメンバを持たず、またそれらの型を継承することもできないため、妥当な定義ですね。
以上が標準レイアウト型及び標準レイアウトクラスの定義になります。
規格の矛盾疑惑
以下のようなクラス$\mathrm{S}$について考えます。
#include <type_traits>
#include <iostream>
struct A {};
struct B : A {};
struct S : A
{
B b;
};
int main()
{
S s;
A& a1 = static_cast<A&>(s);
B& b = s.b;
A& a2 = static_cast<A&>(b);
std::cout << &a1 << std::endl;
std::cout << &a2 << std::endl;
std::cout << &b << std::endl;
std::cout << &s << std::endl;
std::cout << std::boolalpha << std::is_standard_layout_v<S> << std::endl;
return 0;
}
$\mathrm{A}$と$\mathrm{B}$は標準レイアウトクラスです。
$\mathrm{S}$について、標準レイアウトクラスの条件1,2,3,4,6は明らかに満たしています。
条件7について、$M(\mathrm{S})$を考えてみます。$\mathrm{S}$はデータメンバを持つクラス型であり、最初のデータメンバ$\mathrm{b}$の型は$\mathrm{B}$なので、$M(\mathrm{S})$は$\mathrm{B}$と$M(\mathrm{B})$に含まれる要素の集合です。しかし$\mathrm{B}$はデータメンバを持たないクラスのため、$M(\mathrm{B})$は空になり、結果として$M(\mathrm{S})$は$\mathrm{B}$のみを含む集合になります。$M(\mathrm{S})$が継承しているのは$\mathrm{A}$のみであるため、条件7を満たすことになります。
条件5について考えてみましょう。素直に考えると、$\mathrm{S}$が持つ基底クラスサブオブジェクトは$\mathrm{A}$一つのみなので、条件5を満たすように思えます。
一応、データメンバ$\mathrm{b}$の基底である$\mathrm{A}$を$\mathrm{S}$の基底クラスサブオブジェクトであると無理やりみなして、$\mathrm{S}$は$\mathrm{A}$型の基底クラスサブオブジェクトを二つ持っていると解釈することもできなくはないかもしれません。しかしそのような解釈には違和感がありますし、データメンバの基底クラスサブオブジェクトを間接的に基底クラスサブオブジェクトとして持つという解釈を認めてしまうと、以下のようなクラス$\mathrm{S2}$や$\mathrm{S3}$も条件5に違反することになり、標準レイアウトではなくなってしまいます。
struct A {};
struct B : A {};
struct S2 : A
{
int x;
B b;
};
struct C : A {};
struct S3
{
B b;
C c;
};
なお、上記$\mathrm{S2}$および$\mathrm{S3}$はテストした全てのコンパイラで標準レイアウトクラスになりました。
さて、もともとの争点である$\mathrm{S}$に話を戻しましょう。考察の結果、$\mathrm{S}$は標準レイアウトクラスの条件1~7をすべて満たすため、標準レイアウトクラスであるように思えます。実際に、テストした全てのコンパイラで標準レイアウトクラスであると判定されました。
しかし、$\mathrm{a1}$と$\mathrm{a2}$に異なるアドレスを割り当てるためには、$\mathrm{b}$と$\mathrm{s}$のアドレスを異なるものにする必要があるように思えます。その場合、$\mathrm{S}$における最初のデータメンバのオフセットが非ゼロになり、標準レイアウトクラスの特徴に矛盾してしまいます。
実際に、gcc 8.2.0, gcc HEAD, clang 6.0.1, clang HEAD では、$\mathrm{b}$と$\mathrm{s}$のアドレスは異なるものになりました。MSVCでは全て同じアドレスが表示されましたが、これは$\mathrm{A}$型の別のオブジェクトが同一のアドレスを持っているためやはり規格違反です。
この問題について知見をお持ちの方はコメントをくださると大変ありがたいです。
標準レイアウトクラスの特徴
標準レイアウト型、標準レイアウトクラス型として認められると、規格により様々な保証が得られます。
オブジェクトのアドレスと最初のデータメンバのアドレスが同一になるということは繰り返し述べてきましたが、他の特徴もたくさんあります。
- 必ず連続したメモリ領域に配置される
- offsetofマクロを必ず適用可能
- レイアウト互換関連
- ポインタ互換関連
ここでは各特徴について深く触れませんが、レイアウト互換は非常に興味深い概念なので、時間ができれば追記したいです。
まとめ
標準レイアウトクラスの判定はコンパイラも間違えるほど難しい。
- 今回試験した範囲では、clang HEAD のみが規格を正しく実装していました。
そもそも規格がよくわからない。