チュートリアル:どの特別メンバーをいつ書くか

特別なメンバー関数の背後にあるルールを誰かに説明するとき、およびどのルールを記述する必要があるかを説明するときに、この図が常に表示されます。ただし、この図が特に役立つとは思いません.

実際に意味を成すよりもはるかに多くの組み合わせをカバーしています。そのため、特別なメンバー関数について実際に知っておく必要があることと、いつどの組み合わせを記述する必要があるかについて話しましょう。

特別メンバー関数図

問題の図は Howard Hinnant によって作成されました:

説明が必要な点がいくつかあります:

    <リ>

    「ユーザー宣言」の特別なメンバー関数は、何らかの方法で言及された特別なメンバー関数です。 クラス内:定義を持つことができ、 default にすることができます エド、それは delete かもしれません d.これは、foo(const foo&) = default と書くことを意味します。 移動コンストラクターを禁止します。

    <リ>

    「デフォルト」の特殊メンバーと宣言されたコンパイラは、= default と同じように動作します。 、例えばデフォルトのコピー コンストラクター copy はすべてのメンバーを構築します。

    <リ>

    「削除された」特殊メンバーを宣言したコンパイラは、= delete と同じように動作します。 、例えばオーバーロード解決がそのオーバーロードを使用することを決定した場合、削除された関数を呼び出しているというエラーで失敗します。

    <リ>

    コンパイラが特別なメンバーを宣言しない場合、オーバーロードの解決に参加しません。これは、参加する削除されたメンバーとは異なります。たとえば、コピー コンストラクターがある場合、コンパイラーは宣言しません。 コンストラクターを移動します。そのため、T obj(std::move(other)) と記述します。 その結果、コピー コンストラクターが呼び出されます。一方、ムーブ コンストラクターが削除された場合 、ムーブ コンストラクターを選択する書き込みが削除されているため、エラーが発生します。

    <リ>

    赤でマークされたボックスの動作は非推奨です。その場合のデフォルトの動作は危険です。

はい、その図は複雑です。これは、生成規則を示す目的で移動セマンティクスに関する講演で提供されました。

ただし、それらを知る必要はありません。次の状況のどれが当てはまるかを知る必要があるだけです。

大多数のケース:ゼロのルール

class normal
{
public:
    // rule of zero
};

絶対大多数のクラスはデストラクタを必要としません。次に、コピー/移動コンストラクタまたはコピー/移動代入演算子も必要ありません:コンパイラが生成したデフォルトは正しいことを行います™.

これは、ゼロのルールとして知られています。可能な限り、ゼロのルールに従ってください。

コンストラクターがない場合、クラスにはコンパイラが生成したデフォルト コンストラクターが含まれます。コンストラクターがある場合は、そうではありません。その場合、適切なデフォルト値があれば、デフォルト コンストラクターを追加します。

コンテナ クラス:ルール オブ ファイブ (6)

class container
{
public:
    container() noexcept;
    ~container() noexcept;

    container(const container& other);
    container(container&& other) noexcept;

    container& operator=(const container& other);
    container& operator=(container&& other) noexcept;
};

デストラクタを記述する必要がある場合 (たとえば、動的メモリを解放する必要があるため)、コンパイラによって生成されたコピー コンストラクタと代入演算子は間違ったことを行います。その場合は、独自のものを提供する必要があります。

これは 5 の規則として知られています。カスタム デストラクタがあるときはいつでも、セマンティクスが一致するコピー コンストラクタと代入演算子も記述します。パフォーマンス上の理由から、移動コンストラクタと移動代入演算子も記述します。

移動関数は、元のオブジェクトのリソースを盗み、空の状態のままにすることができます。それらをnoexceptにするよう努めてください

コンストラクターができたので、暗黙のデフォルト コンストラクターはありません。ほとんどの場合、移動後のクラスのように、クラスを空の状態にするデフォルト コンストラクターを実装するのが理にかなっています。

これが 6 の法則になります。

リソース ハンドル クラス:移動のみ

class resource_handle
{
public:
    resource_handle() noexcept;
    ~resource_handle() noexcept;

    resource_handle(resource_handle&& other) noexcept;
    resource_handle& operator=(resource_handle&& other) noexcept;

    // resource_handle(const resource_handle&) = delete;
    // resource_handle& operator=(const resource_handle&) = delete;
};

場合によっては、デストラクタを作成する必要がありますが、コピーを実装することはできません。例としては、ファイル ハンドルまたは同様の OS リソースをラップするクラスがあります。

それらのクラスを 移動のみ にする .つまり、デストラクタを作成し、コンストラクタと代入演算子を移動します。

ハワードのチャートを見ると、その場合、コピー コンストラクターと代入演算子が削除されていることがわかります。クラスは移動のみである必要があるため、これは正しいです。明示的にしたい場合は、手動で = delete

繰り返しになりますが、移動後の状態にする既定のコンストラクターを追加することは理にかなっています。

不動クラス

class immoveable
{
public:
    immoveable(const immoveable&) = delete; 
    immoveable& operator=(const immoveable&) = delete;

    // immoveable(immoveable&&) = delete;
    // immoveable& operator=(immoveable&&) = delete;
};

クラスをコピーまたは移動できないようにしたい場合があります。オブジェクトが作成されると、常にそのアドレスに留まります。これは、そのオブジェクトへのポインタを安全に作成したい場合に便利です。

その場合、コピー コンストラクターを削除する必要があります。コンパイラは移動コンストラクターを宣言しません。つまり、すべての種類のコピーまたは移動がコピー コンストラクターを呼び出そうとしますが、これは削除されます。明示的にしたい場合は、手動で = delete

代入演算子も削除する必要があります。オブジェクトを物理的に移動するわけではありませんが、代入はコンストラクタと密接に関連しています。以下を参照してください。

回避:3 のルール

class avoid
{
public:
    ~avoid();

    avoid(const avoid& other);
    avoid& operator=(const avoid& other);
};

コピー操作のみを実装する場合でも、クラスを移動するとコピーが呼び出されます。一般的なコードの多くは、コピー操作よりも移動操作の方が安価であると想定しているため、それを尊重してください。

C++11 をサポートしている場合は、move を実装してパフォーマンスを改善してください。

禁止:コピー専用型

class dont
{
public:
    ~dont();

    dont(const dont& other);
    dont& operator=(const dont& other);

    dont(dont&&) = delete;
    dont& operator=(dont&&) = delete;
};

コピー操作があり、移動操作を手動で削除した場合でも、オーバーロードの解決に参加します。

これは次のことを意味します:

dont a(other);            // okay
dont b(std::move(other)); // error: calling deleted function

これは驚くべきことなので、そうしないでください。

禁止事項:デフォルトのコンストラクタを削除

class dont
{
public:
    dont() = delete;
};

= delete する理由はありません デフォルトのコンストラクターが必要ない場合は、別のコンストラクターを作成してください。

唯一の例外は、どのような方法でも構築できない型ですが、そのような型は、「bottom」または「never」型の言語サポートがなければ、実際には役に立ちません。

だから、やらないでください。

禁止:部分的な実装

class dont
{
public:
    dont(const dont&);
    dont& operator=(const dont&) = delete;
};

コピーの構築とコピーの割り当てはペアです。両方を使用するか、両方を使用しないかのいずれかです。

概念的には、コピー代入はより高速な「破棄 + コピー コンストラクト」サイクルです。そのため、コピー コンストラクトがある場合は、デストラクタの呼び出しとコンストラクションを使用して記述できるため、コピー代入も使用する必要があります。

ジェネリック コードでは、多くの場合、型をコピーできる必要があります。慎重に作成しないと、コピーの作成とコピーの代入を区別できない可能性があります。

コピーでしか構築できず代入できない型、またはその逆の型には哲学的な議論が存在する可能性がありますが、実際的なことを行い、それらを避けてください。

検討:スワップ

class consider
{
public:
    friend void swap(consider& lhs, consider& rhs) noexcept;
};

一部のアルゴリズム、特に移動前のものは swap() を使用します オブジェクトを移動します。タイプが swap() を提供しない場合 std::swap() を使用します。 .

std::swap() 3 つの動きをします:

template <typename T>
void swap(T& lhs, T& rhs)
{
    T tmp(std::move(lhs));
    lhs = std::move(rhs);
    rhs = std::move(tmp);
}

より高速な swap() を実装できれば もちろん、これは、独自のコピーまたは移動を実装したカスタム デストラクタを持つクラスにのみ適用されます。

自分の swap() 常に noexcept である必要があります .

結論

それに基づいて、特別メンバー関数の新しい概要を作成しました:特別メンバー チャート

次回ルールを説明する必要がある場合は、生成図の代わりに、この概要またはこのブログ投稿を使用することを検討してください。