このFAQはアグリゲートと POD に関するもので、次の内容をカバーしています。
- 集合体とは?
- POD (Plain Old Data)とは何ですか?
- それらはどのように関連していますか?
- どのように、そしてなぜ彼らは特別なのですか?
- C++11 の変更点は?
この記事はかなり長いです。集計と POD (Plain Old Data) の両方について知りたい場合は、時間をかけて読んでください。集計だけに興味がある場合は、最初の部分だけを読んでください。POD のみに興味がある場合は、最初に集計の定義、意味、および例を読んでから、POD にジャンプする必要がありますが、最初の部分全体を読むことをお勧めします。集計の概念は、POD を定義するために不可欠です。エラーを見つけた場合 (文法、文体、書式設定、構文など、些細なことでも) コメントを残してください。編集します。
この回答は C++03 に適用されます。その他の C++ 標準については、次を参照してください。
C++ 標準 ( C++03 8.5.1 §1 )からの正式な定義:
集合体は、ユーザーが宣言したコンストラクター (12.1)、プライベートまたは保護された非静的データ メンバー (条項 11)、基本クラス (条項 10)、および仮想関数 (10.3) を持たない配列またはクラス (条項 9) です。 )。
では、この定義を解析しましょう。まず、配列は集合体です。クラスは、次の場合に集約することもできます…待ってください! 構造体や共用体については何も言われていませんが、それらは集合体ではないでしょうか? はい、できます。C++ では、この用語class
はすべてのクラス、構造体、および共用体を指します。したがって、クラス (または構造体、または共用体) は、上記の定義の基準を満たす場合にのみ集約です。これらの基準は何を意味しますか?
これは、集約クラスがコンストラクターを持つことができないという意味ではありません。実際、ユーザーによって明示的にではなく、コンパイラーによって暗黙的に宣言されている限り、デフォルト コンストラクターやコピー コンストラクターを持つことができます。
プライベートまたは保護された非静的データ メンバーはありません。プライベートまたは保護されたメンバー関数 (コンストラクターは除く) と、プライベートまたは保護された静的データ メンバーおよびメンバー関数を好きなだけ持つことができ、集約クラスの規則に違反することはありません。
集約クラスは、ユーザー宣言/ユーザー定義のコピー代入演算子および/またはデストラクタを持つことができます
配列は、非集合体クラス型の配列であっても集合体です。
次に、いくつかの例を見てみましょう。
class NotAggregate1
{
virtual void f() {} //remember? no virtual functions
};
class NotAggregate2
{
int x; //x is private by default and non-static
};
class NotAggregate3
{
public:
NotAggregate3(int) {} //oops, user-defined constructor
};
class Aggregate1
{
public:
NotAggregate1 member1; //ok, public member
Aggregate1& operator=(Aggregate1 const & rhs) {/* */} //ok, copy-assignment
private:
void f() {} // ok, just a private function
};
あなたはアイデアを得る。それでは、集計がどのように特別であるかを見てみましょう。これらは、非集約クラスとは異なり、中かっこで初期化できます{}
。この初期化構文は配列で一般的に知られており、これらが集合体であることを学習しました。それでは、それらから始めましょう。
Type array_name[n] = {a1, a2, …, am};
if(m == n)
配列
の i番目の要素は a iで初期化され
ます if(m < n)
配列の最初の m 個の要素は a 1、a 2、…、a mおよびその他のn - m
要素可能であれば、値が初期化されます(用語の説明については以下を参照してください)
else if(m > n)
コンパイラはエラーを発行します
else (これは n がまったく指定されていない場合です like int a[] = {1, 2, 3};
)
のサイズ配列 (n) は m に等しいと想定されるため、次int a[] = {1, 2, 3};
と同等です。int a[3] = {1, 2, 3};
bool
スカラー型 ( 、int
、char
、double
、ポインターなど)のオブジェクトが値で初期化0
される場合、その型 ( false
for bool
、0.0
forなど)で初期化されることを意味しますdouble
。ユーザー宣言のデフォルト コンストラクターを持つクラス型のオブジェクトが値初期化されると、そのデフォルト コンストラクターが呼び出されます。デフォルトのコンストラクターが暗黙的に定義されている場合、すべての非静的メンバーは再帰的に値が初期化されます。この定義は不正確で少し間違っていますが、基本的な考え方は理解できるはずです。参照を値で初期化することはできません。たとえば、クラスに適切な既定のコンストラクターがない場合、非集約クラスの値の初期化は失敗する可能性があります。
配列の初期化の例:
class A
{
public:
A(int) {} //no default constructor
};
class B
{
public:
B() {} //default constructor available
};
int main()
{
A a1[3] = {A(2), A(1), A(14)}; //OK n == m
A a2[3] = {A(2)}; //ERROR A has no default constructor. Unable to value-initialize a2[1] and a2[2]
B b1[3] = {B()}; //OK b1[1] and b1[2] are value initialized, in this case with the default-ctor
int Array1[1000] = {0}; //All elements are initialized with 0;
int Array2[1000] = {1}; //Attention: only the first element is 1, the rest are 0;
bool Array3[1000] = {}; //the braces can be empty too. All elements initialized with false
int Array4[1000]; //no initializer. This is different from an empty {} initializer in that
//the elements in this case are not value-initialized, but have indeterminate values
//(unless, of course, Array4 is a global array)
int array[2] = {1, 2, 3, 4}; //ERROR, too many initializers
}
集約クラスをブレースで初期化する方法を見てみましょう。ほぼ同じ方法です。配列要素の代わりに、非静的データ メンバーをクラス定義での出現順に初期化します (定義によりすべてパブリックです)。メンバーより初期化子が少ない場合、残りは値で初期化されます。明示的に初期化されていないメンバーの 1 つを値で初期化できない場合、コンパイル時エラーが発生します。必要以上の初期化子がある場合、コンパイル時エラーも発生します。
struct X
{
int i1;
int i2;
};
struct Y
{
char c;
X x;
int i[2];
float f;
protected:
static double d;
private:
void g(){}
};
Y y = {'a', {10, 20}, {20, 30}};
上記の例y.c
では、 、 with 、 with 、 with 、 with で初期化され'a'
てy.x.i1
おり10
、y.x.i2
値20
でy.i[0]
初期20
化y.i[1]
さ30
れy.f
ています。つまり、 で初期化されてい0.0
ます。保護された静的メンバーd
は、 であるため、まったく初期化されませんstatic
。
集約共用体は、中括弧で最初のメンバーのみを初期化できるという点で異なります。共用体の使用を検討できるほど C++ に精通している場合 (共用体の使用は非常に危険であり、慎重に検討する必要があります)、標準の共用体のルールを自分で調べることができると思います :)。
集約の特別な点がわかったので、クラスの制限を理解してみましょう。つまり、彼らがそこにいる理由です。中括弧を使用したメンバーごとの初期化は、クラスがそのメンバーの合計にすぎないことを意味することを理解する必要があります。ユーザー定義のコンストラクターが存在する場合、ユーザーがメンバーを初期化するために追加の作業を行う必要があることを意味するため、ブレースの初期化は正しくありません。仮想関数が存在する場合は、このクラスのオブジェクトが (ほとんどの実装で) クラスのいわゆる vtable へのポインターを持っていることを意味します。これはコンストラクターで設定されるため、ブレースの初期化では不十分です。演習と同様の方法で残りの制限を把握できます:)。
骨材については十分です。これで、より厳密なタイプのセット、つまり POD を定義できるようになりました
C++ 標準 ( C++03 9 §4 )からの正式な定義:
POD 構造体は、非 POD 構造体、非 POD 共用体 (またはそのような型の配列) 型、または参照型の非静的データ メンバーを持たず、ユーザー定義のコピー代入演算子を持たない集約クラスです。ユーザー定義のデストラクタ。同様に、POD 共用体は、非 POD 構造体、非 POD 共用体 (またはそのような型の配列) 型、または参照型の非静的データ メンバーを持たず、ユーザー定義のコピー代入演算子を持たない集約共用体です。ユーザー定義のデストラクタはありません。POD クラスは、POD 構造体または POD 共用体のいずれかであるクラスです。
うわー、これは解析するのが難しいですね。:) 共用体を除外して (上記と同じ理由で)、もう少し明確な方法で言い換えてみましょう:
ユーザー定義のコピー代入演算子とデストラクタがなく、その非静的メンバーが非 POD クラス、非 POD の配列、または参照ではない場合、集約クラスは POD と呼ばれます。
この定義は何を意味しますか? ( PODはPlain Old Dataの略だと言いましたか?)
例:
struct POD
{
int x;
char y;
void f() {} //no harm if there's a function
static std::vector<char> v; //static members do not matter
};
struct AggregateButNotPOD1
{
int x;
~AggregateButNotPOD1() {} //user-defined destructor
};
struct AggregateButNotPOD2
{
AggregateButNotPOD1 arrOfNonPod[3]; //array of non-POD class
};
POD クラス、POD 共用体、スカラー型、およびそのような型の配列は、まとめてPOD 型と呼ばれます。
POD は多くの点で特別です。いくつかの例を紹介します。
POD クラスは、C 構造体に最も近いものです。それらとは異なり、POD はメンバー関数と任意の静的メンバーを持つことができますが、これら 2 つのどちらもオブジェクトのメモリ レイアウトを変更しません。したがって、C や .NET からも使用できる多かれ少なかれ移植性のある動的ライブラリを作成する場合は、エクスポートされたすべての関数が POD 型のパラメーターのみを受け取り、返すようにする必要があります。
非 POD クラス タイプのオブジェクトの有効期間は、コンストラクタが終了したときに始まり、デストラクタが終了したときに終了します。POD クラスの場合、有効期間はオブジェクトのストレージが占有されたときに始まり、そのストレージが解放または再利用されたときに終了します。
memcpy
POD 型のオブジェクトの場合、オブジェクトの内容を char または unsigned char の配列に格納し、memcpy
その内容をオブジェクトに戻すと、オブジェクトは元の値を保持することが標準で保証されています。非 POD タイプのオブジェクトにはそのような保証がないことに注意してください。また、POD オブジェクトを安全にコピーできますmemcpy
。次の例では、T が POD タイプであると想定しています。
#define N sizeof(T)
char buf[N];
T obj; // obj initialized to its original value
memcpy(buf, &obj, N); // between these two calls to memcpy,
// obj might be modified
memcpy(&obj, buf, N); // at this point, each subobject of obj of scalar type
// holds its original value
goto ステートメント。ご存じかもしれませんが、変数がまだスコープ内にないポイントから、既にスコープ内にあるポイントに goto を介してジャンプすることは違法です (コンパイラーはエラーを発行する必要があります)。この制限は、変数が非 POD タイプの場合にのみ適用されます。次の例f()
では、は整形式ですが、g()
は整形式です。Microsoft のコンパイラは、この規則に対して寛大すぎることに注意してください。どちらの場合も警告を出すだけです。
int f()
{
struct NonPOD {NonPOD() {}};
goto label;
NonPOD x;
label:
return 0;
}
int g()
{
struct POD {int i; char c;};
goto label;
POD x;
label:
return 0;
}
POD オブジェクトの先頭にパディングがないことが保証されています。つまり、POD クラス A の最初のメンバーが T 型の場合、安全に最初のメンバーへのポインターreinterpret_cast
からポインターA*
をT*
取得したり、その逆を行ったりすることができます。
リストは延々と続きます…</p>
ご覧のとおり、多くの言語機能は POD に対して異なる動作をするため、POD とは何かを正確に理解することが重要です。
集計の標準的な定義は少し変更されましたが、ほとんど同じです。
集合体は、ユーザー提供のコンストラクター (12.1)、非静的データ メンバーのブレースまたはイコール初期化子(9.2)、プライベートまたは保護された非静的データ メンバー (条項 11)、基本クラスなし (条項 10)、および仮想関数なし (10.3)。
わかりました、何が変わったのですか?
以前は、集約にはユーザー宣言のコンストラクターを含めることができませんでしたが、ユーザー指定のコンストラクターを含めることができなくなりました。違いはありますか?はい、あります。コンストラクターを宣言してデフォルトにすることができるようになったからです。
struct Aggregate {
Aggregate() = default; // asks the compiler to generate the default implementation
};
最初の宣言でデフォルト設定されたコンストラクター (または特別なメンバー関数)はユーザー提供ではないため、これはまだ集約です。
現在、集約は、非静的データ メンバーのブレースまたはイコール初期化子を持つことができません。これは何を意味するのでしょうか?これは、この新しい標準により、次のようにクラスでメンバーを直接初期化できるためです。
struct NotAggregate {
int x = 5; // valid in C++11
std::vector<int> s{1,2,3}; // also valid
};
この機能を使用すると、基本的に独自のデフォルト コンストラクターを提供することと同等であるため、クラスは集合体ではなくなります。
つまり、集計とはまったく変わりませんでした。基本的な考え方はそのままに、新しい機能に対応しています。
POD には多くの変更が加えられました。この新しい標準では、POD に関する以前の規則の多くが緩和され、標準で定義が提供される方法が根本的に変更されました。
POD の考え方は、基本的に 2 つの異なるプロパティをキャプチャすることです。
このため、定義は 2 つの異なる概念に分割されています。単純なクラスと標準レイアウトクラスです。これらは POD よりも便利だからです。現在、標準では POD という用語はほとんど使用されておらず、より具体的な単純で標準的なレイアウトの概念が好まれています。
新しい定義は基本的に、POD は単純で標準的なレイアウトを持つクラスであり、このプロパティはすべての非静的データ メンバーに対して再帰的に保持する必要があることを示しています。
POD 構造体は、自明クラスと標準レイアウト クラスの両方である非共用体クラスであり、型非 POD 構造体、非 POD 共用体 (またはそのような型の配列) の非静的データ メンバーはありません。同様に、POD 共用体は自明クラスと標準レイアウト クラスの両方である共用体であり、型非 POD 構造体、非 POD 共用体 (またはそのような型の配列) の非静的データ メンバーはありません。POD クラスは、POD 構造体または POD 共用体のいずれかであるクラスです。
これら 2 つのプロパティを個別に詳しく見ていきましょう。
Trivialは上記の最初のプロパティです。trivial クラスは静的初期化をサポートします。クラスが自明にコピー可能 (自明なクラスのスーパーセット) である場合、その表現をその場所に次のようにコピーしてmemcpy
、結果が同じであることを期待しても問題ありません。
標準では、自明なクラスを次のように定義しています。
自明にコピー可能なクラスとは、次のようなクラスです。
— 重要なコピー コンストラクターがない (12.8)。
— 重要なムーブ コンストラクターがない (12.8)。
— 自明でないコピー代入演算子を持たない (13.5.3, 12.8),
— 自明でない移動代入演算子がない (13.5.3、12.8)、および
— 自明なデストラクタがあります (12.4)。
自明なクラスは、自明なデフォルト コンストラクター (12.1) を持ち、自明にコピー可能なクラスです。
[注:特に、自明にコピー可能または自明なクラスには、仮想関数または仮想基底クラスがありません。—終わりのメモ]
では、これらの些細なことと重要なことは何ですか?
クラス X のコピー/移動コンストラクターは、それがユーザー提供でなく、
— クラス X には仮想関数 (10.3) も仮想基本クラス (10.1) もありません。
— 各直接基底クラス サブオブジェクトをコピー/移動するために選択されたコンストラクターは自明であり、
— クラス型 (またはその配列) である X の非静的データ メンバーごとに、そのメンバーをコピー/移動するために選択されたコンストラクターは自明です。
それ以外の場合、コピー/移動コンストラクターは自明ではありません。
基本的にこれは、コピーまたは移動コンストラクターがユーザーによって提供されない場合、クラスに仮想要素がなく、このプロパティがクラスのすべてのメンバーと基本クラスに対して再帰的に保持される場合、自明であることを意味します。
単純なコピー/移動代入演算子の定義は非常に似ており、「コンストラクター」という単語を「代入演算子」に置き換えるだけです。
自明なデストラクタにも同様の定義があり、仮想化できないという制約が追加されています。
さらに別の同様のルールが自明なデフォルト コンストラクターに存在します。さらに、上記で見たように、クラスに、 brace-or-equal-initializersを持つ非静的データ メンバーがある場合、デフォルト コンストラクターは自明ではありません。
すべてを明確にするためのいくつかの例を次に示します。
// empty classes are trivial
struct Trivial1 {};
// all special members are implicit
struct Trivial2 {
int x;
};
struct Trivial3 : Trivial2 { // base class is trivial
Trivial3() = default; // not a user-provided ctor
int y;
};
struct Trivial4 {
public:
int a;
private: // no restrictions on access modifiers
int b;
};
struct Trivial5 {
Trivial1 a;
Trivial2 b;
Trivial3 c;
Trivial4 d;
};
struct Trivial6 {
Trivial2 a[23];
};
struct Trivial7 {
Trivial6 c;
void f(); // it's okay to have non-virtual functions
};
struct Trivial8 {
int x;
static NonTrivial1 y; // no restrictions on static members
};
struct Trivial9 {
Trivial9() = default; // not user-provided
// a regular constructor is okay because we still have default ctor
Trivial9(int x) : x(x) {};
int x;
};
struct NonTrivial1 : Trivial3 {
virtual void f(); // virtual members make non-trivial ctors
};
struct NonTrivial2 {
NonTrivial2() : z(42) {} // user-provided ctor
int z;
};
struct NonTrivial3 {
NonTrivial3(); // user-provided ctor
int w;
};
NonTrivial3::NonTrivial3() = default; // defaulted but not on first declaration
// still counts as user-provided
struct NonTrivial5 {
virtual ~NonTrivial5(); // virtual destructors are not trivial
};
standard-layoutは 2 番目のプロパティです。標準は、これらが他の言語との通信に役立つと述べています。これは、標準レイアウト クラスが同等の C 構造体または共用体と同じメモリ レイアウトを持っているためです。
これは、メンバーとすべての基本クラスに対して再帰的に保持する必要がある別のプロパティです。そしていつものように、仮想関数や仮想基本クラスは許可されていません。これにより、レイアウトが C と互換性がなくなります。
ここでの緩やかなルールは、標準レイアウト クラスはすべての非静的データ メンバーが同じアクセス制御を持つ必要があるということです。以前は、これらはすべてpublicである必要がありましたが、すべてがprivate またはすべてprotectedである限り、それらを private または protected にすることができるようになりました。
継承を使用する場合、継承ツリー全体で 1 つのクラスのみが非静的データ メンバーを持つことができ、最初の非静的データ メンバーを基本クラス型にすることはできません (これはエイリアシング規則に違反する可能性があります)。レイアウトクラス。
標準テキストでの定義は次のとおりです。
標準レイアウト クラスは、次のようなクラスです。
— 非標準レイアウト クラス (またはそのような型の配列) 型または参照型の非静的データ メンバーを持たない、
— 仮想関数 (10.3) も仮想基本クラス (10.1) もありません。
— すべての非静的データメンバーに対して同じアクセス制御 (条項 11) を持ちます。
— 非標準レイアウトの基本クラスはありません。
— 最も派生したクラスに非静的データ メンバーがなく、非静的データ メンバーを持つ基本クラスが最大で 1 つあるか、または非静的データ メンバーを持つ基本クラスがない。
— 最初の非静的データ メンバーと同じ型の基本クラスがありません。
標準レイアウト構造体は、class-key 構造体または class-key クラスで定義された標準レイアウト クラスです。
標準レイアウト ユニオンは、クラス キー ユニオンで定義された標準レイアウト クラスです。
[注:標準レイアウト クラスは、他のプログラミング言語で記述されたコードとの通信に役立ちます。それらのレイアウトは 9.2 で指定されています。—終わりのメモ]
そして、いくつかの例を見てみましょう。
// empty classes have standard-layout
struct StandardLayout1 {};
struct StandardLayout2 {
int x;
};
struct StandardLayout3 {
private: // both are private, so it's ok
int x;
int y;
};
struct StandardLayout4 : StandardLayout1 {
int x;
int y;
void f(); // perfectly fine to have non-virtual functions
};
struct StandardLayout5 : StandardLayout1 {
int x;
StandardLayout1 y; // can have members of base type if they're not the first
};
struct StandardLayout6 : StandardLayout1, StandardLayout5 {
// can use multiple inheritance as long only
// one class in the hierarchy has non-static data members
};
struct StandardLayout7 {
int x;
int y;
StandardLayout7(int x, int y) : x(x), y(y) {} // user-provided ctors are ok
};
struct StandardLayout8 {
public:
StandardLayout8(int x) : x(x) {} // user-provided ctors are ok
// ok to have non-static data members and other members with different access
private:
int x;
};
struct StandardLayout9 {
int x;
static NonStandardLayout1 y; // no restrictions on static members
};
struct NonStandardLayout1 {
virtual f(); // cannot have virtual functions
};
struct NonStandardLayout2 {
NonStandardLayout1 X; // has non-standard-layout member
};
struct NonStandardLayout3 : StandardLayout1 {
StandardLayout1 x; // first member cannot be of the same type as base
};
struct NonStandardLayout4 : StandardLayout3 {
int z; // more than one class has non-static data members
};
struct NonStandardLayout5 : NonStandardLayout3 {}; // has a non-standard-layout base class
これらの新しいルールにより、より多くのタイプを POD にできるようになりました。また、タイプが POD でない場合でも、POD プロパティの一部を個別に利用できます (それが自明または標準レイアウトの 1 つにすぎない場合)。
標準ライブラリには、ヘッダーでこれらのプロパティをテストするための特性があります<type_traits>
。
template <typename T>
struct std::is_pod;
template <typename T>
struct std::is_trivial;
template <typename T>
struct std::is_trivially_copyable;
template <typename T>
struct std::is_standard_layout;
C++17 国際標準の最終ドラフトをここからダウンロードしてください。
凝集体
C++17 では、集計と集計の初期化が拡張および強化されています。標準ライブラリには、std::is_aggregate
型特性クラスも含まれるようになりました。以下は、セクション 11.6.1.1 および 11.6.1.2 からの正式な定義です (内部参照は省略されています)。
集約は配列またはクラスであり、
— ユーザー提供、明示的、または継承されたコンストラクターは
ありません — プライベートまたは保護された非静的データ メンバーは
ありません — 仮想関数はなく、
— 仮想、プライベート、または保護された基本クラスはありません。
[ 注: 集約の初期化では、保護されたプライベート基本クラスのメンバーまたはコンストラクターにアクセスできません。—エンドノート]
集合体の要素は次のとおり です
。
匿名共用体のメンバー (宣言順)。
何が変わったのですか?
struct B1 // not a aggregate
{
int i1;
B1(int a) : i1(a) { }
};
struct B2
{
int i2;
B2() = default;
};
struct M // not an aggregate
{
int m;
M(int a) : m(a) { }
};
struct C : B1, B2
{
int j;
M m;
C() = default;
};
C c { { 1 }, { 2 }, 3, { 4 } };
cout
<< "is C aggregate?: " << (std::is_aggregate<C>::value ? 'Y' : 'N')
<< " i1: " << c.i1 << " i2: " << c.i2
<< " j: " << c.j << " m.m: " << c.m.m << endl;
//stdout: is C aggregate?: Y, i1=1 i2=2 j=3 m.m=4
struct D // not an aggregate
{
int i = 0;
D() = default;
explicit D(D const&) = default;
};
struct B1
{
int i1;
B1() : i1(0) { }
};
struct C : B1 // not an aggregate
{
using B1::B1;
};
自明なクラス
C++14 では対処されなかったいくつかの欠陥に対処するために、C++17 では自明クラスの定義が作り直されました。変更は本質的に技術的なものでした。12.0.6 での新しい定義は次のとおりです (内部参照は省略されています)。
自明にコピー可能なクラスは次のようなクラスです:
— 各コピー コンストラクタ、ムーブ コンストラクタ、コピー代入演算子、およびムーブ代入演算子は、削除済みまたは自明のいずれかです
— 少なくとも 1 つの削除されていないコピー コンストラクタ、ムーブ コンストラクタ、コピー代入演算子、またはムーブ代入演算子、および
— それには、削除されていない単純なデストラクタがあります。
自明なクラスは、自明にコピー可能であり、1 つ以上のデフォルト コンストラクターを持ち、そのすべてが自明または削除され、少なくとも 1 つが削除されていないクラスです。[注: 特に、自明にコピー可能なクラスまたは自明なクラスには、仮想関数または仮想基底クラスがありません。—末尾の注]
変更点:
std::memcpy
ます。クラスの作成者は、すべてのコンストラクター/代入演算子を削除済みとして定義することにより、クラスをコピー/移動できないことを明確に意図していたため、これは意味論的矛盾でしたが、クラスは依然として自明にコピー可能なクラスの定義を満たしています。したがって、C++17 では、自明にコピー可能なクラスには、少なくとも 1 つの自明で削除されていない (必ずしもパブリックにアクセスできるとは限らない) コピー/移動コンストラクタ/代入演算子が必要であるという新しい句があります。N4148、DR1734を参照標準レイアウト クラス
standard-layout の定義も、欠陥レポートに対処するために作り直されました。ここでも変更は本質的に技術的なものでした。これは標準(12.0.7)のテキストです。前と同じように、内部参照は省略されます。
クラス S は、次の場合に標準レイアウト クラスです。
— 非標準レイアウト クラス (またはそのようなタイプの配列) 型または参照の非静的データ メンバーを持た
ない。 — 仮想関数も仮想基底クラスも持たない。
—すべての非静的データ メンバーに対して同じアクセス制御を持っている
— 非標準レイアウトの基本クラスを持たない —
任意の型の最大で 1 つの基本クラス サブオブジェクトを
持つ — すべての非静的データ メンバーとビット フィールドを持つ同じクラスで最初に宣言されたクラスとその基本クラス、および
— 基本クラスとしての型 (以下で定義) の集合 M(S) の要素を持たない.108
M(X) は次のように定義される:
— X が (継承される可能性がある) 非静的データ メンバーを持たない非共用体クラス型の場合、セット M(X) は空です。
— X が非共用体クラス型で、最初の非静的データ メンバーが型 X0 (このメンバーは無名共用体である場合があります) を持つ場合、セット M(X) は X0 と M(X0) の要素で構成されます。
— X が共用体型の場合、セット M(X) はすべての M(Ui) とすべての Ui を含むセットの共用体であり、各 Ui は X の i 番目の非静的データ メンバーの型です。
— X の場合要素型が Xe の配列型である場合、セット M(X) は Xe と M(Xe) の要素で構成されます。
— X が非クラス、非配列型の場合、集合 M(X) は空です。
[ 注: M(X) は、標準レイアウト クラスで X のゼロ オフセットにあることが保証されているすべての非基本クラス サブオブジェクトの型のセットです
。
--end example ]struct B { int i; }; // standard-layout class struct C : B { }; // standard-layout class struct D : C { }; // standard-layout class struct E : D { char : 4; }; // not a standard-layout class struct Q {}; struct S : Q { }; struct T : Q { }; struct U : S, T { }; // not a standard-layout class
108) これにより、同じクラス型を持ち、同じ最も派生したオブジェクトに属する 2 つのサブオブジェクトが同じアドレスに割り当てられないことが保証されます。
変更点:
注: C++ 標準委員会は、公開された C++14 標準には新しい言語が含まれていませんが、欠陥レポートに基づいて上記の変更を C++14 に適用することを意図していました。これは C++17 標準にあります。
ここでは、C++11 の POD は基本的に、自明性とレイアウトという 2 つの異なる軸に分割されていました。自明性とは、オブジェクトの概念的価値とそのストレージ内のデータのビットとの間の関係に関するものです。レイアウトとは、オブジェクトのサブオブジェクトのレイアウトです。クラス型のみがレイアウトを持ち、すべての型は自明関係を持ちます。
したがって、自明軸とは次のようなものです。
自明でないコピー可能: このような型のオブジェクトの値は、オブジェクト内に直接格納されているバイナリ データ以上のものである可能性があります。
たとえば、 ;をunique_ptr<T>
格納します。T*
これは、オブジェクト内のバイナリ データの全体です。しかし、それは aの値の全体ではありませんunique_ptr<T>
。Aunique_ptr<T>
には、nullptr
またはインスタンスによって有効期間が管理されるオブジェクトへのポインタが格納されunique_ptr<T>
ます。その管理は の価値の一部ですunique_ptr<T>
。そして、その値はオブジェクトのバイナリ データの一部ではありません。そのオブジェクトのさまざまなメンバー関数によって作成されます。
たとえば、a へnullptr
の代入unique_ptr<T>
は、オブジェクトに格納されているビットを変更するだけではありません。このような割り当ては、 によって管理されるすべてのオブジェクトを破棄する必要がありunique_ptr
ます。unique_ptr
メンバー関数を介さずにの内部ストレージを操作すると、このメカニズムが損傷T*
し、現在管理しているオブジェクトを破壊せずに内部を変更すると、オブジェクトが持つ概念的な価値に違反します。
簡単にコピー可能: このようなオブジェクトの値は、バイナリ ストレージの内容とまったく同じです。これが、そのバイナリ ストレージのコピーをオブジェクト自体のコピーと同等にすることを合理的にする理由です。
単純なコピー可能性 (単純なデストラクタ、単純な/削除されたコピー/移動コンストラクタ/割り当て) を定義する特定の規則は、型がバイナリ値のみであるために必要なものです。の場合のように、オブジェクトのデストラクタは、オブジェクトの「値」の定義に参加できますunique_ptr
。そのデストラクタが単純な場合、オブジェクトの値の定義には関与しません。
特殊なコピー/移動操作も、オブジェクトの値に参加できます。unique_ptr
の move コンストラクターは、移動操作のソースを null にすることで変更します。unique_ptr
これにより、 a の値が一意であることが保証されます。単純なコピー/移動操作は、そのようなオブジェクト値の悪ふざけが実行されていないことを意味するため、オブジェクトの値は、格納されているバイナリ データのみにすることができます。
自明: このオブジェクトは、格納するすべてのビットに機能値があると見なされます。自明にコピー可能とは、オブジェクトのデータ ストアの意味を、まさにそのデータであると定義します。しかし、そのようなタイプは、データがそこに到達する方法を (ある程度) 制御できます。このような型には、特定のメンバーが常に特定の値を持つことを保証する既定のメンバー初期化子および/または既定のコンストラクターを含めることができます。したがって、オブジェクトの概念的な値は、格納できるバイナリ データのサブセットに制限できます。
単純なデフォルト コンストラクターを持つ型でデフォルトの初期化を実行すると、そのオブジェクトは完全に初期化されていない値のままになります。そのため、自明なデフォルト コンストラクターを持つ型は、そのデータ ストレージ内の任意のバイナリ データに対して論理的に有効です。
レイアウト軸は実にシンプルです。コンパイラには、クラスのサブオブジェクトをクラスのストレージ内に格納する方法を決定する際に、多くの裁量が与えられます。ただし、この余裕が必要ない場合もあり、より厳密な順序保証が役立つ場合があります。
このようなタイプは、標準のレイアウト タイプです。そして、C++ 標準は、そのレイアウトが具体的に何であるかについてはあまり言及していません。基本的に、標準のレイアウト タイプについて次の 3 つのことを述べています。
最初のサブオブジェクトは、オブジェクト自体と同じアドレスにあります。
を使用offsetof
して、外部オブジェクトからそのメンバー サブオブジェクトの 1 つまでのバイト オフセットを取得できます。
union
アクティブなメンバーがアクセスされている非アクティブなメンバーと同じレイアウトを (少なくとも部分的に) 使用している場合、共用体の非アクティブなメンバーを介してサブオブジェクトにアクセスするいくつかのゲームをプレイできます。
コンパイラは通常、標準レイアウト オブジェクトをstruct
C の同じメンバーを持つ型にマップすることを許可します。しかし、C++ 標準にはそのような記述はありません。それはまさにコンパイラがやりたいと感じていることです。
この時点では、PODは基本的に役に立たない用語です。これは、単純なコピー可能性 (値はバイナリ データのみ) と標準レイアウト (サブオブジェクトの順序がより明確に定義されている) の交点にすぎません。そのようなことから、型が C に似ており、同様の C オブジェクトにマップできると推測できます。しかし、この規格には、その趣旨の記述はありません。
次のルールについて詳しく教えてください。
私が試してみます:
a) 標準レイアウト クラスには、同じアクセス制御を持つすべての非静的データ メンバーが必要です。
それは簡単です: すべての非静的データ メンバーは、すべてpublic
、private
、またはでなければなりませんprotected
。あなたはいくつかpublic
を持つことはできませんprivate
。
それらの理由は、「標準レイアウト」と「非標準レイアウト」をまったく区別する理由につながります。つまり、コンパイラーに、物事をメモリーに入れる方法を選択する自由を与えるためです。vtable ポインターだけではありません。
彼らが 98 年に C++ を標準化したとき、彼らは基本的に人々がそれをどのように実装するかを予測しなければなりませんでした。彼らは C++ のさまざまなフレーバーでかなりの実装経験を持っていましたが、物事について確信が持てませんでした。そこで彼らは用心深く、コンパイラーに可能な限りの自由を与えることにしました。
そのため、C++98 での POD の定義は非常に厳密です。これにより、C++ コンパイラは、ほとんどのクラスのメンバー レイアウトに関して大きな自由度を得ることができました。基本的に、POD 型は特殊なケースを意図しており、理由があって具体的に書いたものです。
C++11 に取り組んでいたとき、彼らはコンパイラの経験が豊富でした。そして彼らは、C++ コンパイラの作成者は本当に怠け者であることに気付きました。彼らはこの自由をすべて持っていましたが、それに対して何もしませんでした。
標準レイアウトのルールは、多かれ少なかれ一般的な慣行を体系化したものです。ほとんどのコンパイラは、それらを実装するために、ほとんど変更する必要はありませんでした (おそらく、対応する型特性のためのいくつかのものを除いて)。
さて、public
/private
になると、事情は異なります。どのメンバーが実際にあるのかを並べ替える自由はpublic
、private
特にビルドのデバッグにおいて、コンパイラーにとって重要です。また、標準レイアウトのポイントは他の言語との互換性があるため、デバッグとリリースでレイアウトを変えることはできません。
次に、ユーザーを実際に傷つけないという事実があります。カプセル化されたクラスを作成している場合は、すべてのデータ メンバーがカプセル化される可能性が高くなりprivate
ます。通常、完全にカプセル化された型のパブリック データ メンバーは公開しません。したがって、これは、それを行いたい、その分割を望んでいる少数のユーザーにとってのみ問題になります。
なので大した損ではありません。
b) 継承ツリー全体で 1 つのクラスのみが非静的データ メンバーを持つことができます。
この理由は、彼らが再び標準レイアウトを標準化した理由、つまり一般的な慣行に戻ります。
実際に物事を格納する継承ツリーの 2 つのメンバーを持つことに関しては、一般的な方法はありません。派生クラスの前に基本クラスを置く人もいれば、逆の方法で行う人もいます。メンバーが 2 つの基本クラスから来ている場合、メンバーをどのように並べますか? 等々。コンパイラは、これらの質問で大きく異なります。
また、0/1/無限の規則のおかげで、メンバーを持つクラスを 2 つ持つことができると言ったら、好きなだけ言うことができます。これを処理するには、多くのレイアウト ルールを追加する必要があります。多重継承がどのように機能するか、どのクラスが他のクラスの前にデータを配置するかなどを説明する必要があります。これは多くの規則であり、実質的な利益はほとんどありません。
仮想関数とデフォルトのコンストラクタ標準レイアウトを持たないものをすべて作成することはできません。
また、最初の非静的データ メンバーを基本クラス型にすることはできません (これにより、エイリアシング ルールが破られる可能性があります)。
私は本当にこれと話すことができません。私は、C++ のエイリアシング ルールを本当に理解できるほど十分に教育を受けていません。ただし、基本メンバーが基本クラス自体と同じアドレスを共有するという事実と関係があります。あれは:
struct Base {};
struct Derived : Base { Base b; };
Derived d;
static_cast<Base*>(&d) == &d.b;
そして、それはおそらく C++ のエイリアシング規則に反しています。何らかの方法で。
ただし、これを考慮してください。これを行う機能が実際にどれほど役立つでしょうか? 1 つのクラスのみが非静的データ メンバーを持つことができるため、Derived
そのクラスでなければなりません (Base
メンバーとして を持っているため)。したがって、(データの) 空でBase
なければなりません。そして、基本クラスと同様にBase
空の場合...なぜそれのデータメンバーがあるのですか?
Base
は空なので、状態はありません。this
したがって、非静的メンバー関数は、ポインターではなく、パラメーターに基づいて実行します。
繰り返しますが、大きな損失はありません。