明示的に既定された関数および削除された関数

C++11 では、特殊なメンバー関数を自動的に生成するかどうかを、既定化および削除指定関数によって明示的に制御できます。 また、削除された関数を使用すると、すべての型の関数 (特殊なメンバー関数、通常のメンバー関数、非メンバー関数) への引数で問題のある型の昇格が発生するのを防ぐための単純な言語も提供されます。これにより、不要な関数呼び出しが発生します。

明示的な既定化および削除指定関数の利点

C++ では、コンパイラが独自のコンストラクターを宣言しない場合、型の既定のコンストラクター、コピー コンストラクター、コピー代入演算子、デストラクターを自動的に生成します。 これらの関数は特殊なメンバー関数と呼ばれ、C++ の単純なユーザー定義型が C の構造体と同様に動作します。つまり、追加のコーディング作業を行うことなく、作成、コピー、破棄できます。 C++11 では、移動セマンティクスが、言語に取り込まれた後、移動コンストラクターと移動代入演算子が、コンパイル時に自動生成可能な特殊なメンバー関数のリストに追加されます。

これは単純型の場合には便利ですが、複合型ではそれ自体によって特殊なメンバー関数が頻繁に定義されるために、他の特殊なメンバー関数が自動的に生成されなくなる可能性があります。 実際には、次の処理が行われています。

  • コンストラクターが明示的に宣言されると、既定のコンストラクターが自動的に生成されなくなります。

  • 仮想デストラクターが明示的に宣言されると、既定のデストラクターが自動的に生成されなくなります。

  • 移動コンストラクターまたは移動代入演算子が明示的に宣言されると、次のようになります。

    • コピー コンストラクターが自動的に生成されません。

    • コピー代入演算子が自動的に生成されません。

  • コピー コンストラクター、コピー代入演算子、移動コンストラクター、移動代入演算子、またはデストラクターが明示的に宣言されると、次のようになります。

    • 移動コンストラクターが自動的に生成されません。

    • 移動代入演算子が自動的に生成されません。

Note

さらに、C++11 標準では次の追加の規則が定義されています。

  • コピー コンストラクターまたはデストラクターが明示的に宣言されている場合、コピー代入演算子の自動生成は非推奨とされます。
  • コピー代入演算子またはデストラクターが明示的に宣言されている場合は、コピー コンストラクターの自動生成は非推奨とされます。

どちらの場合も、Visual Studio は引き続き必要な関数を暗黙的に自動的に生成し、既定では警告を生成しません。 Visual Studio 2022 バージョン 17.7 以降では、 C5267 を有効にして警告を出力できます。

また、これらの規則の結果がオブジェクト階層にリークすることがあります。 たとえば、何らかの理由で基底クラスに、派生クラスから呼び出し可能な既定のコンストラクター (つまり、publicprotectedパラメーターを受け取らないコンストラクター) が存在しない場合、そこから派生したクラスは、独自の既定のコンストラクターを自動的に生成できません。

これらの規則は、単純なユーザー定義型と共通の C++ イディオムの実装を複雑にする可能性があります。たとえば、コピー コンストラクターとコピー代入演算子をプライベートに宣言し、定義しないことによって、ユーザー定義型をコピー不可能にすることができます。

struct noncopyable
{
  noncopyable() {};

private:
  noncopyable(const noncopyable&);
  noncopyable& operator=(const noncopyable&);
};

C++11 より前のコード スニペットは、コピー不可能な型の慣用的な形式でした。 しかし、今では次のような複数の問題があります。

  • コピー コンストラクターを非表示にするためにはプライベートに宣言する必要があります。しかしどのような形でも宣言した以上、既定のコンストラクターは生成されません。 したがって、既定のコンストラクターが必要な場合は、空のコンストラクターであっても何かを明示的に定義する必要があります。

  • 明示的に定義された既定のコンストラクターが何も行わない場合でも、コンパイラはそれを非トリビアルと見なします。 この実装方法は、既定のコンストラクターを自動的に生成するより非効率で、noncopyable が真の POD 型になる妨げになります。

  • コピー コンストラクターまたはコピー代入演算子が外部コードから不可視であっても、noncopyable のフレンドからは可視であり、呼び出すことができます。 宣言されているが定義されていない場合は、呼び出すとリンカー エラーが発生します。

  • これは一般的に受け入れられるイディオムですが、特別なメンバー関数の自動生成に関するすべてのルールを理解していない限り、意図は明確ではありません。

C++11 では、より簡単な方法で、コピー不可能なイディオムを実装できます。

struct noncopyable
{
  noncopyable() =default;
  noncopyable(const noncopyable&) =delete;
  noncopyable& operator=(const noncopyable&) =delete;
};

以前の表現形式の問題に対する C++11 の解決策を次に説明します。

  • コピー コンストラクターを宣言して既定のコンストラクターの生成を防止できるのはこれまでどおりですが、明示的な既定化によりコンストラクターを既定に戻すことができます。

  • 明示的に既定の特殊なメンバー関数は引き続き単純と見なされるため、パフォーマンスの低下はなく、 noncopyable 実際の POD 型になることは防止されません。

  • コピー コンストラクターとコピー代入演算子は、パブリックですが削除されています。 削除された関数を定義または呼び出すと、コンパイル時エラーになります。

  • プログラムの意図は、=default=delete を理解していれば明確です。 特殊なメンバー関数の自動生成の規則を理解する必要はありません。

移動できない、動的に割り当てることができる、または動的に割り当てることができないユーザー定義型を作成する場合にも、同様のイディオムが存在します。 これらの表現形式は C++11 以前の方法で実装され、同様の問題を抱えていますが、C++11 で既定化および削除指定の特殊なメンバー関数として実装することにより、同様に解決されました。

明示的な既定化関数

特別なメンバー関数を既定に設定して、特別なメンバー関数が既定の実装を使用することを明示的に示したり、非パブリック アクセス修飾子を使用して特殊なメンバー関数を定義したり、他の状況で自動生成が禁止されていた特殊なメンバー関数を復元したりできます。

特殊なメンバー関数を既定にするには、次の例のように宣言します。

struct widget
{
  widget()=default;

  inline widget& operator=(const widget&);
};

inline widget& widget::operator=(const widget&) =default;

インライン可能な場合は、クラス本体の外側にある特殊なメンバー関数も既定にできることに注意してください。

特殊なメンバー関数が自明であることにはパフォーマンス上のメリットがあるため、既定の動作が必要な場合は、空の関数本体を作成するよりも、特殊なメンバー関数を自動生成することを推奨します。 これには、特殊なメンバー関数を明示的に既定にする方法と、まったく宣言しない方法 (同時に他の特殊なメンバー関数も宣言せず、自動的に生成されないようにします) のいずれかを選択できます。

削除指定関数

特殊なメンバー関数と通常のメンバー関数、および非メンバー関数を削除して、定義または呼び出されないようにすることができます。 特殊なメンバー関数を削除すると、必要のない特殊なメンバー関数がコンパイラから生成されるのを簡単に防止できます。 関数は宣言時に削除する必要があります。後で関数を宣言し、後で既定値にできる方法で削除することはできません。

struct widget
{
  // deleted operator new prevents widget from being dynamically allocated.
  void* operator new(std::size_t) = delete;
};

通常のメンバー関数または非メンバー関数を削除すると、問題のある型の昇格によって意図しない関数が呼び出されるのを防ぐことができます。 関数は削除された後もオーバーロードの解決に関与し、型の上位変換の後に呼び出されるよりも的確に照合できるためこの方法は効果的です。 関数呼び出しによって、より具体的な、しかし削除済みの関数に解決され、コンパイラ エラーが発生します。

// deleted overload prevents call through type promotion of float to double from succeeding.
void call_with_true_double_only(float) =delete;
void call_with_true_double_only(double param) { return; }

前のサンプルでは、引数を使用floatして呼び出call_with_true_double_onlyすとコンパイラ エラーが発生しますが、引数intint使用した呼び出しcall_with_true_double_onlyは失敗することに注意してください。この場合、引数は関数のバージョンからintdouble昇格され、正常に呼び出doubleされます。ただし、それが意図した内容ではない可能性があります。 double 以外の引数を使用してこの関数を呼び出すとコンパイラ エラーが発生することを確認するには、削除された関数のテンプレート バージョンを宣言します。

template < typename T >
void call_with_true_double_only(T) =delete; //prevent call through type promotion of any T to double from succeeding.

void call_with_true_double_only(double param) { return; } // also define for const double, double&, etc. as needed.