集約と単純型/PODとは何ですか? また、それらはどのように/なぜ特別なのですか? 質問する

集約と単純型/PODとは何ですか? また、それらはどのように/なぜ特別なのですか? 質問する

これよくある質問集計と POD に関するもので、次の内容をカバーしています。

  • 集計とは何ですか?
  • POD (Plain Old Data) とは何ですか?
  • 最近では、自明な型または自明にコピー可能な型とは何でしょうか?
  • それらはどのように関連しているのでしょうか?
  • それらはどのように、そしてなぜ特別なのでしょうか?
  • C++11 では何が変わりますか?

ベストアンサー1

読み方:

この記事はかなり長いです。集約と 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番目の要素はiで初期化されます
。else if(m < n)
配列の最初の m 要素は1、 a 2、…、 a mで初期化され、他のn - m要素は、可能であれば値で初期化されます(用語の説明については下記を参照)
。else if(m > n)
コンパイラはエラーを発行します
。else (n が のようにまったく指定されていない場合int a[] = {1, 2, 3};)
配列のサイズ (n) は m と等しいとみなされるため、次int a[] = {1, 2, 3};の式と同等ですint a[3] = {1, 2, 3};

boolスカラー型 ( 、intchardouble、ポインターなど)のオブジェクトが値初期化される0ということは、その型に対して ( 、などに対してfalse)を使用して初期化されることを意味します。ユーザー宣言のデフォルト コンストラクターを持つクラス型のオブジェクトが値初期化されると、そのデフォルト コンストラクターが呼び出されます。デフォルト コンストラクターが暗黙的に定義されている場合、すべての非静的メンバーが再帰的に値初期化されます。この定義は不正確で少し間違っていますが、基本的な考え方は伝わるはずです。参照は値初期化できません。非集約クラスの値初期化は、たとえばクラスに適切なデフォルト コンストラクターがない場合に失敗する可能性があります。bool0.0double

配列の初期化の例:

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で初期化され'a'y.x.i110で、は で、は で、 はで初期化され、つまり で初期化されます。protected static メンバーはであるため、まったく初期化されません。y.x.i220y.i[0]20y.i[1]30y.f0.0dstatic

集合共用体は、中括弧を使用して最初のメンバーのみを初期化できるという点で異なります。共用体の使用を検討できるほど C++ に精通している場合は (共用体の使用は非常に危険な場合があり、慎重に検討する必要があります)、標準で共用体のルールを自分で調べることができると思います :)。

集約の特殊性がわかったので、クラスの制限、つまり制限が存在する理由を理解してみましょう。中括弧を使用したメンバー単位の初期化は、クラスがそのメンバーの合計にすぎないことを意味することを理解する必要があります。ユーザー定義のコンストラクターが存在する場合、メンバーを初期化するためにユーザーが追加の作業を行う必要があるため、中括弧による初期化は不適切です。仮想関数が存在する場合、このクラスのオブジェクトには (ほとんどの実装で) コンストラクターで設定されるクラスのいわゆる vtable へのポインターがあるため、中括弧による初期化では不十分です。残りの制限も同様の方法で演習として理解できます :)。

集約についてはこれで十分です。今度は、より厳密な型のセット、つまりPODを定義します。

PODとは何か、なぜ特別なのか

C++標準(C++03 9 §4からの正式な定義:

POD 構造体は、非 POD 構造体、非 POD 共用体 (またはそのような型の配列)、または参照型の非静的データ メンバーを持たず、ユーザー定義のコピー代入演算子とユーザー定義のデストラクタを持たない集約クラスです。同様に、POD 共用体は、非 POD 構造体、非 POD 共用体 (またはそのような型の配列)、または参照型の非静的データ メンバーを持たず、ユーザー定義のコピー代入演算子とユーザー定義のデストラクタを持たない集約共用体です。POD クラスは、POD 構造体または POD 共用体のいずれかであるクラスです。

うわー、これは解析するのが難しいですね。:) ユニオンを省略して (上記と同じ理由で)、もう少しわかりやすい言い方で言い換えてみましょう。

集約クラスは、ユーザー定義のコピー代入演算子とデストラクタを持たず、その非静的メンバーが非 POD クラス、非 POD の配列、または参照でない場合、POD と呼ばれます。

この定義は何を意味するのでしょうか? ( POD はPlain Old Dataの略だと言いましたか?)

  • すべてのPODクラスは集約です。逆に言えば、クラスが集約でない場合は、PODではないことは確かです。
  • クラスは、構造体と同様にPODになることができますが、どちらの場合も標準的な用語はPOD-structです。
  • 集約の場合と同様に、クラスにどのような静的メンバーがあるかは関係ありません。

例:

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 クラスとは異なり、POD はメンバー関数と任意の静的メンバーを持つことができますが、これら 2 つはどちらもオブジェクトのメモリ レイアウトを変更しません。したがって、C や .NET から使用できる、ある程度移植性の高い動的ライブラリを作成する場合は、エクスポートされたすべての関数が POD 型のパラメーターのみを受け取って返すようにしてください。

  • 非 POD クラス タイプのオブジェクトの有効期間は、コンストラクターが終了したときに始まり、デストラクタが終了したときに終わります。POD クラスの場合、有効期間はオブジェクトのストレージが占有されたときに始まり、そのストレージが解放または再利用されたときに終わります。

  • memcpyPOD 型のオブジェクトの場合、オブジェクトの内容を 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*て最初のメンバーへのポインターを取得できます。その逆も同様です。

リストはまだまだ続きます…

結論

ご覧のとおり、多くの言語機能は POD によって動作が異なるため、POD が正確に何であるかを理解することが重要です。

おすすめ記事