C++ コア ガイドライン:コンストラクター

各オブジェクトのライフサイクルは、その作成から始まります。したがって、この投稿では、オブジェクトの最も基本的な 13 のルール、コンストラクター ルールについて説明します。

1 つの投稿に 12 のルールは多すぎます。したがって、最初の 11 個のみを取り上げます。ルールを 10 個だけにしないのはなぜですか。 11番目のルールが面白すぎるからです。残りの 2 つは、次の投稿の一部です。これが 13 のルールです。

コンストラクタ ルール:

  • C.40:クラスに不変式がある場合はコンストラクタを定義する
  • C.41:コンストラクタは完全に初期化されたオブジェクトを作成する必要があります
  • C.42:コンストラクタが有効なオブジェクトを構築できない場合、例外をスローします
  • C.43:値型クラスにデフォルトのコンストラクターがあることを確認する
  • C.44:デフォルトのコンストラクタはシンプルでスローしないものを好む
  • C.45:データ メンバーを初期化するだけのデフォルト コンストラクタを定義しないでください。代わりにメンバー初期化子を使用してください
  • C.46:デフォルトでは、引数が 1 つのコンストラクターを宣言する explicit
  • C.47:メンバー宣言の順序でメンバー変数を定義および初期化する
  • C.48:定数初期化子のコンストラクターでは、メンバー初期化子よりもクラス内初期化子を優先します
  • C.49:コンストラクターでは代入よりも初期化を優先
  • C.50:初期化中に「仮想動作」が必要な場合は、ファクトリ関数を使用してください
  • C.51:委任コンストラクタを使用して、クラスのすべてのコンストラクタに共通のアクションを表す
  • C.52:継承コンストラクタを使用して、さらに明示的な初期化を必要としない派生クラスにコンストラクタをインポートする

それでは、ルールを詳しく見ていきましょう。さらに分析するには、ルールへのリンクを使用してください。

C.40:クラスに不変条件がある場合はコンストラクタを定義する

オブジェクトの不変条件は、オブジェクトの存続期間全体にわたって保持されるべきオブジェクトの特性です。そのような不変条件を確立する場所はコンストラクターです。不変式は有効な日付になる可能性があります。

class Date { // a Date represents a valid date
 // in the January 1, 1900 to December 31, 2100 range
 Date(int dd, int mm, int yy)
 :d{dd}, m{mm}, y{yy}
 {
 if (!is_valid(d, m, y)) throw Bad_date{}; // enforce invariant
 }
 // ...
private:
 int d, m, y;
};

C.41:コンストラクタは完全に初期化されたオブジェクトを作成する必要があります

このルールは、前のルールと非常によく似ています。したがって、完全に初期化されたオブジェクトを作成するのは、コンストラクターの仕事です。 init メソッドを持つクラスが問題を起こしています。

class X1 {
 FILE* f; // call init() before any other function
 // ...
public:
 X1() {}
 void init(); // initialize f
 void read(); // read from f
 // ...
};

void f()
{
 X1 file;
 file.read(); // crash or bad read!
 // ...
 file.init(); // too late
 // ...
}

ユーザーが誤って init の前に read を呼び出したり、単に init の呼び出しを忘れたりする可能性があります。

C.42:コンストラクターが有効なオブジェクトを構築できない場合、スローする例外

前の規則に従って、有効なオブジェクトを構築できない場合は例外をスローします。追加することはあまりありません。無効なオブジェクトを操作する場合は、オブジェクトを使用する前に常にオブジェクトの状態を確認する必要があります。これは非常にエラーが発生しやすいです。以下はガイドラインの例です:

class X3 { // bad: the constructor leaves a non-valid object behind
 FILE* f; 
 bool valid;
 // ...
public:
 X3(const string& name)
 :f{fopen(name.c_str(), "r")}, valid{false}
 {
 if (f) valid = true;
 // ...
 }

 bool is_valid() { return valid; }
 void read(); // read from f
 // ...
};

void f()
{
 X3 file {"Heraclides"};
 file.read(); // crash or bad read!
 // ...
 if (file.is_valid()) {
 file.read();
 // ...
 }
 else {
 // ... handle error ...
 }
 // ...
}

C.43:値型クラスにデフォルト コンストラクターがあることを確認する

値型は、int のように動作する型です。値型は、通常の型に似ています。具象型に関する投稿で、値型と通常の型について書きました。デフォルトのコンストラクターを使用すると、型を簡単に使用できます。 STL コンテナーの多くのコンストラクターは、型に既定のコンストラクターがあるという事実に依存しています。たとえば、std::map などの順序付き連想コンテナーの値の場合です。クラスのすべてのメンバーがデフォルトのコンストラクターを持っている場合、コンパイラーは暗黙的にクラスのコンストラクターを生成します。

C.44:デフォルト コンストラクターはシンプルでスローしないことを優先する

スローできないデフォルトのコンストラクターを使用すると、エラー処理がはるかに簡単になります。ガイドラインでは簡単な例を示しています:

template<typename T>
// elem is nullptr or elem points to space-elem element allocated using new
class Vector1 {
public:
 // sets the representation to {nullptr, nullptr, nullptr}; doesn't throw
 Vector1() noexcept {}
 Vector1(int n) :elem{new T[n]}, space{elem + n}, last{elem} {}
 // ...
private:
 own<T*> elem = nullptr;
 T* space = nullptr;
 T* last = nullptr;
};

C.45:Don'データメンバーのみを初期化するデフォルトのコンストラクターを定義します。代わりにメンバー初期化子を使用してください

これは、C++11 の私のお気に入りの機能の 1 つです。クラス本体でクラス メンバーを直接定義すると、コンストラクターの記述がはるかに簡単になり、場合によっては時代遅れになります。クラス X1 はそのメンバーを古典的な方法 (C++11 より前) で定義し、X2 は好ましい方法で定義します。良い副作用は、コンパイラが X2 のコンストラクタを自動的に生成することです。

class X1 { // BAD: doesn't use member initializers
 string s;
 int i;
public:
 X1() :s{"default"}, i{1} { }
 // ...
};

class X2 {
 string s = "default";
 int i = 1;
public:
 // use compiler-generated default constructor
 // ...
};

C.46:デフォルトでは、単一引数のコンストラクタを宣言する explicit

これは非常に重要なルールです。単一引数のコンストラクターは、多くの場合、変換コンストラクターと呼ばれます。それらを明示的にしないと、暗黙の変換が発生する可能性があります。

class String {
public:
 explicit String(int); // explicit
 // String(int); // implicit
};

String s = 10; // error because of explicit 

コンストラクターが明示的であるため、int から String への暗黙的な変換を使用することはできません。明示的なコンストラクターの代わりに、アウトコメントされた暗黙的なコンストラクターが使用される場合、サイズ 10 の文字列が得られます

C.47:メンバー変数をメンバーの順序で定義および初期化する宣言

クラス メンバーは、宣言の順序で初期化されます。コンストラクタ初期化子で異なる順序でそれらを初期化すると、驚くかもしれません.

class Foo {
 int m1;
 int m2;
public:
 Foo(int x) :m2{x}, m1{++x} { } // BAD: misleading initializer order
 // ...
};

Foo x(1); // surprise: x.m1 == x.m2 == 2

C.48:メンバー初期化子よりもクラス内初期化子を優先する定数イニシャライザのコンストラクタ内

クラス内初期化子により、コンストラクターの定義がはるかに簡単になります。さらに、メンバーの初期化を忘れることはできません。

class X { // BAD
 int i;
 string s;
 int j;
public:
 X() :i{666}, s{"qqq"} { } // j is uninitialized
 X(int ii) :i{ii} {} // s is "" and j is uninitialized
 // ...
};

class X2 {
 int i {666};
 string s {"qqq"};
 int j {0};
public:
 X2() = default; // all members are initialized to their defaults
 X2(int ii) :i{ii} {} // s and j initialized to their defaults (1)
 // ...
};

クラス内の初期化によってオブジェクトの既定の動作が確立されますが、コンストラクター (1) によって既定の動作のバリエーションが許可されます。

C.49:コンストラクターでの代入よりも初期化を優先する

それはかなり古いルールです。割り当てへの初期化の最も明白な長所は、値を割り当てて初期化せずに使用することを忘れないことと、初期化が割り当てよりも高速であっても遅くなることはありません。

class B { // BAD
 string s1;
public:
 B() { s1 = "Hello, "; } // BAD: default constructor followed by assignment
 // ...
};

C.50:「仮想動作」が必要な場合は、ファクトリ関数を使用します」 初期化中

コンストラクターから仮想関数を呼び出すと、期待どおりに動作しません。保護上の理由から、派生クラスの作成が行われていないため、仮想呼び出しメカニズムはコンストラクターで無効になっています。

したがって、次の例では、仮想関数 f のベース バージョンが呼び出されます。

// virtualConstructor.cpp

#include <iostream>

struct Base{
 Base(){
 f();
 }
 virtual void f(){
 std::cout << "Base called" << std::endl;
 }
};

struct Derived: Base{
 virtual void f(){
 std::cout << "Derived called" << std::endl;
 }
};

int main(){
 
 std::cout << std::endl;
 
 Derived d; 
 
 std::cout << std::endl;
 
};

これがプログラムの出力です。

それでは、オブジェクトの初期化中に仮想動作を持つファクトリ関数を作成しましょう。所有権を処理するために、ファクトリ関数は std::unique_ptr や std::shared_ptr などのスマート ポインターを返す必要があります。出発点として、前の例を使用しますが、Base のコンストラクターを保護します。したがって、Derived クラスのオブジェクトのみを作成できます。

// virtualInitialisation.cpp

#include <iostream>
#include <memory>

class Base{
protected:
 Base() = default;
public:
 virtual void f(){ // (1)
 std::cout << "Base called" << std::endl; 
 }
 template<class T> 
 static std::unique_ptr<T> CreateMe(){ // (2) 
 auto uniq = std::make_unique<T>();
 uniq->f(); // (3)
 return uniq;
 }
 virtual ~Base() = default; // (4)
};

struct Derived: Base{
 virtual void f(){
 std::cout << "Derived called" << std::endl;
 }
};


int main(){
 
 std::cout << std::endl;
 
 std::unique_ptr<Base> base = Derived::CreateMe<Derived>(); // (5)
 
 std::cout << std::endl;
 
};

初期化の最後に、仮想関数 f (1) を呼び出す必要があります。 (2) はファクトリ関数です。このファクトリ関数は、std::unique_ptr を作成した後に f を呼び出し、それを返します。 Derived が Base から派生している場合、std::unique_ptr は std::unique_ptr に暗黙的に変換可能です。最後に、初期化中に仮想動作を取得します。

この手法には 1 つのリスクがあります。ベースがスコープ外になった場合は、Derived のデストラクタが呼び出されるようにする必要があります。これが Base (4) の仮想デストラクタの理由です。デストラクタが仮想でない場合、未定義の動作が発生します。奇妙ですが、ファクトリ メソッドに std::unique_ptr の代わりに std::shared_ptr を使用した場合、Base の仮想デストラクタは必要ありません。

次は?

申し訳ありませんが、投稿は少し長すぎます。しかし、特に最後の規則 (C.50) は非常に興味深いものでした。したがって、私はいつもより多くを説明しなければなりませんでした。次の投稿では、コンストラクターのルールを終了し、コピーと移動のルールから始めます。