ムーブ セマンティクスとデフォルト コンストラクタ – 6 つのルール?

かなり前のことです - 4 週間以上前です。 - 移動の安全性について書きました。

この投稿は、moved-from 状態の動作に依存するべきか、それとも何らかの保証を行うべきかについて、多くの議論を引き起こしました。詳細については、この CppChat エピソードの前半を参照してください。

しかし、私はその議論を続けるつもりはありません。どちらの側にも説得力のある議論があり、ここで一方の側を擁護したくはありません.

代わりに、C++ の移動セマンティクスに関連する別のことについて話します。これは、元の投稿には収まりませんでした:デフォルト コンストラクターと移動セマンティクスの間の関係。

C++98 の 3 の規則

C++98 には 3 つのルールがありました:デストラクタ/コピー コンストラクタ/コピー代入演算子を定義する場合、他の 2 つも定義する必要があります。

通常、デストラクタを持つクラスは何らかのクリーンアップを行う必要があります。クラスは、解放する必要がある何らかの形式のリソースを所有しています。また、クラスがリソースを所有している場合は、それをコピーする前に特別な注意を払う必要もあります。

デストラクタを持つ C++98 クラスがある場合、コピー コンストラクタ/代入演算子には 2 つの適切なオプションがあります。

<オール> <リ>

それを「削除」し、クラスのコピーを禁止します。

<リ>

リソースのディープ コピーまたは何らかの形式の参照カウントを実行するように定義します。

これまでのところ、単純すぎます。

C++11 の 5 の規則

C++11 ではムーブ セマンティクスが追加されたため、3 つのルールが 5 つのルールになりました (デストラクタ/コピー コンストラクタ/コピー代入/ムーブ コンストラクタ/ムーブ代入)。

一般に、Move は、元のオブジェクトがもう必要ない場合のコピーの最適化と見なすことができます。その後、元のオブジェクトのリソースを盗むことで「コピー」することができます - Move.

さらに、move セマンティクスは move-only タイプを許可します。ほとんどの「リソース」は適切にコピーできず、コピーを無効にすると、関数からリソース ホルダーを返すことができません。しかし、move を使用すると、両方の問題を解決できます。コピーの代わりにリソースを盗み、返すことができます。関数から。

Move-only 型は、Move セマンティクスがもたらした最も有用な機能であると私は考えています。しかし、Move セマンティクスを導入すると、セマンティクスに微妙な変化があります。

C++98 の例

ソケットを処理するための C API の C++ ラッパーであるソケット クラスを考えてみます。C++98 では、次のようになります。

class socket
{
public:
 socket(…)
 : socket_handle_(open_socket(…))
 {
 if (!is_valid(socket_handle_))
 throw invalid_socket_exception(…);
 }

 ~socket()
 {
 close_socket(socket_handle_);
 }

 … 

private:
 socket(const socket&); // no definition
 socket& operator=(const socket&); // no definition

 native_handle socket_handle_;
};

いくつかのパラメータを指定してソケットを開くコンストラクタと、ソケットを閉じるデストラクタがあります。ソケットをコピーする方法がないため、コピー操作は「削除」されます。

ユーザー エラーを防ぐために、ソケットはコンストラクターで有効性がチェックされることに注意してください。05 のみ 有効なハンドルを持つオブジェクトを作成できます。16 したがって、クラスは 決して 「空」、つまり、無効なソケット ハンドルを格納することはなく、常に明確なセマンティクスがあります。ユーザーが 21 を取得した場合

これは API の優れた機能です。

39 を移行中 C++11へ

早送りして13年。 48

しかし、C++11 のおかげで、ムーブ セマンティクスという解決策があります。ある日、開発者は先に進み、ムーブ コンストラクターとムーブ代入演算子をソケット クラスに追加します。実装は、元のオブジェクトからソケットを自然に無効にします。新しいものはそれを破壊します.

それで…話は終わりですか?

いいえ

2 つの移動操作を追加することは悪い考えであり、重大な変更です。最悪の種類の重大な変更:コードは引き続きコンパイルされ、コードは 動作 します。 - 誰かが次のようなコードを書くまで:

socket my_socket(…);
…
socket your_socket(std::move(my_socket));
…
do_sth(my_socket);

move-from ソケットを 57 に渡します .上で述べたように:移動元のソケットには 無効 があります ハンドル、これは単に移動元オブジェクトの性質です。61 は、無効なソケット オブジェクトが渡されることを期待しておらず、それを処理する準備ができていません。 つい最近までソケット オブジェクトが無効でした - 空にならないという保証がありました。

ここで、そのようなコードを書くのは悪い考えであり、そのようなコードを書くべきではないと主張できます。

同意します。これはです 悪い考えです。

しかし、それは重要ではありません。ポイントは、移動セマンティクスの導入のおかげで、クラスのセマンティクス全体が変更されたことです。現在、抽象化に穴があります。以前は、すべてのオブジェクトが有効であることが保証されていました。空でない状態。現在、この保証は破られています。

移動操作の導入により、クラスのセマンティクスが変更され、主な保証が弱体化しました。現在これ

移動セマンティクスの結果

ムーブ セマンティクスをクラスに導入すると、このクラスのセマンティクスが変わります。モデル化される前は 70 、現在は 85 をモデル化しています :リソースがない場合もあります。空になることもあります。

しかし、すべてのクラスで、移動操作の導入によってセマンティクスが変わるわけではありません.Take 99 、移動操作は、元のオブジェクトを有効だが未指定の状態のままにする、非常に優れた追加機能です。 - 前回の投稿で紹介した用語を維持するための基本的な動きの安全性 - それは空のコンテナである可能性が最も高い.なぜそうなのか?

シンプル:104 常にモデル化された 115 .要素のない状態は常に明確に定義され、インターフェイスの一部でした.移動セマンティクスは、それを取得する別の方法を追加しただけで、導入しませんでした.

これで、ようやくタイトルのデフォルト コンストラクターに到達できます。

デフォルト コンストラクタの意味

デフォルト コンストラクターは、適切かつ有効なデフォルト値で型を初期化する必要があります。リソースを所有するクラス、つまりムーブ セマンティクスが意味を持つクラスの場合、これは通常、リソースを持たない状態です。これは、リソース-デフォルトのコンストラクターを持つクラスは、移動操作がなくても、常に「空」状態を処理する必要があります!

したがって、デフォルトのコンストラクタを持つリソース クラスがある場合は、保証を弱めることなくムーブ セマンティクスを導入できます。チェッカー関数とデフォルト コンストラクター。

デフォルトのコンストラクターを追加する/インターフェースを「空の」状態に対応させると、空の状態があり、それを処理する必要があることがクラスのユーザーにとってより明確になります。デフォルトのコンストラクターは、ユーザーに明示的にオブジェクトを「空」状態にします。

オブジェクトを明示的に「空」状態にする必要があるのはなぜですか?

私は「適切に初期化できる場合にのみオブジェクトを定義する」ことに賛成ですが、主に不適切な API を扱う場合など、それが必要になる状況がいくつかあります。

また、ムーブ セマンティクスは既にインターフェイスの保証を弱めているため、デフォルト コンストラクターの導入による (追加の) 害はありません。

結論

6 のルールを導入するつもりはありません。デフォルトのコンストラクターを使用したくない場合がいくつかあります。それに従わなくても悪い結果はありません。移動セマンティクスにより、「空」状態を作成できます。また、すでに「空」状態がある場合は、インターフェース全体を採用します。そして、デフォルト コンストラクターを導入することもお勧めします。

この問題全体が発生するのは、C++ には破壊的な移動がないためです。コンパイラは、移動元オブジェクトの再利用を妨げていません。また、コンパイラがあなたを妨げていない場合、一部のユーザーはいつの日か (偶然に) それを行うでしょう。移動元の状態を完全に無効な状態として扱うことを主張しても、その規則は適用されないため、役に立ちません。

したがって、ムーブ セマンティクスでは、決して空にならないという保証を実際に行うことはできません。これは良いことではありません。しかし、少なくともインターフェースを採用して、空にできることを示すことはできます。