移動の安全 – 移動元の状態で何ができるかを知る

C++ プログラマーには、例外の安全性というこの概念があります。 .これは非常に便利な概念です。これを使用すると、関数がスローされた場合の事後条件を簡単に記述できます。

いくつかの事後条件を簡単に記述する必要がある別の状況があります:移動操作の後のオブジェクトの状態について話すとき、つまり、移動コンストラクターまたは移動代入演算子の後です。関数の例外安全性に似た右側の引数:移動安全性

例外安全性は、関数が例外をスローした場合の関数の事後条件を記述します。同様に、移動安全性は、移動操作後のオブジェクトの事後条件を記述します。オブジェクトから。

移動の安全性が必要な理由

移動セマンティクスを使用すると、オブジェクトをコピーする必要があるが、元のオブジェクトはもう必要ない場合に、より効率的なコードを指定できます。他のオブジェクトのリソースを単純に盗むことができます。いずれにしても、それは破棄されます。移動されるオブジェクトは右辺値であるため、一時的なものであり、すぐに破棄されます。では、なぜ移動操作後にその状態を指定する必要があるのでしょうか?

移動コンストラクターまたは代入演算子が呼び出されるたびに呼び出されるわけではないため、元のオブジェクトは純粋な一時オブジェクトです。キャストによって作成された「人工的な」一時オブジェクトで呼び出されることがあります。これが std::move() です。 そのような場合、操作後にオブジェクトを再度使用したい場合があります。その場合、オブジェクトで何ができるかを正確に知ることは役に立ちます。

そのために、移動の安全性の概念を提案します。例外の安全性レベルと非常によく似ているため、同じ命名スキームに従う 4 つのレベルを識別しました。

これらのレベルは、安全性の高い順に並べられています:

1.不動保証:コピーのみ

移動コンストラクターまたは代入演算子が移動操作を実行しない場合、型は移動なしの安全性を提供します。これらの型の移動はコピーと同等です。

型がこの保証を提供する場合、それは凝ったユーザー定義の移動操作を持たず、同様にこの保証を提供するメンバーのみを持ちます。そのような型は通常、解放する必要があるリソースを所有していないため、特別なコピー操作はありません。またはデストラクタ。

単純な型には、移動操作が暗黙的に生成されない型と同様に、この保証があります。

2.強力な移動の安全性:明確に定義された有効な移動元状態

強力な移動安全性を提供する型の移動元状態は明確に定義されています。事前条件が定義された状態によって満たされているすべてのメンバー関数を安全に呼び出すことができます。さらに、これらのメンバー関数には決定論的な効果/結果があります。

強力な移動の安全性を提供する型の例は std::unique_ptr です .Move の構築は、[unique.ptr]/4 で定義されている「所有権の譲渡」として定義されています

std::unique_ptr の後 operator bool() は移動されましたが、何も所有していません。 false を返します 、 get() nullptr を返します operator*() を呼び出してはいけません または operator->() .

3.基本的な移動の安全性:有効だが未指定の移動元状態

基本的な移動の安全性では、移動元の状態が明確に定義されている必要はありません。移動元の状態が有効であることのみが必要です。 、しかし正確な状態は指定されていません。すべてのメンバー関数を広い契約で安全に呼び出すことができます。つまり、オブジェクトに特別な前提条件はありません。しかし、それらの関数が返す結果は保証されていません;それらは効果/結果ですストロング ムーブ セーフティにあったため、決定論的ではありません。

基本的な動きの安全性を提供する型の例は std::string です .次のコードを見てみましょう:

auto a = "Hello World!"s;
auto b = std::move(a);
std::cout << a.c_str() << '\n';

このプログラムの出力は何ですか?

<オール> <リ>

(空行)

<リ>

Hello World!

<リ>

C++ is weird

<リ>

(セグメンテーション違反)

答えは:std::string::c_str() 前提条件がなく、オブジェクトが有効な状態のままであるため、オプション 4 にはなりません。関数を安全に呼び出すことができます。ただし、他の答えのいずれかである可能性があります。

文字列 Hello World! の場合 std::string によって動的に割り当てられました ,move の構築はおそらくポインターを調整するだけなので、moved-from オブジェクトは空であり、オプション 1 を出力します。しかし、std::string のほとんどの実装は 小さな文字列の最適化 (SSO) と呼ばれるものを使用します。次に、動的な割り当てなしで小さな文字列を格納できる静的バッファーがあります。この場合、移動コンストラクターは、各文字を 1 つの SSO バッファーから手動で 1 つの SSO バッファーにコピーするよりも効率的な移動を行うことができません。他の SSO バッファー。さらに効率を高めるために、実装はスチールされたバッファーをゼロにしない場合があります。この場合、出力はオプション 2 になります。

したがって、結果の状態は有効ですが、正確にはわかりません。

基本移動の保証は、特に指定がない限り、すべての型に対して標準ライブラリが保証するものでもあります。

4.移動の安全性なし:「破壊的な」移動

最低限の保証は移動なしの安全性を提供します。移動元オブジェクトはもはや有効ではありません。デストラクタを呼び出すか、新しい値を割り当てることしかできません。

これは、事後条件について何も保証しない「例外なしの安全性」以上のものであることに注意してください。コンパイラはする 自分で呼び出してください!

また、割り当ては概念的には破棄して再度作成することと同じであるため、同様に許可する必要があると判断しました。

型にはどの保証を提供する必要がありますか?

リソースを所有していないタイプの場合、それらは自動的に移動なしの保証を提供します。

リソースを所有するタイプ (実際に移動操作が必要な場合) については、実用的でありながら実装が最も高速であることを保証します。移動操作はコピーの最適化と見なすことができます。ストロング ムーブ セーフティを簡単に実装できる場合は、それを実行します。基本ムーブ セーフティよりも手間がかかる場合は、ベーシック セーフティのみを提供することを検討してください。オブジェクトがどの状態にあるかわからないため、ベーシック セーフティはストロング セーフティよりも明らかに役に立ちません。ので、必要な場合にのみ行ってください。

リソースを所有するタイプには、リソースを所有するか、リソースを所有しないという 2 つの基本的な状態があります。移動 (またはデフォルトの構築) により、リソースを所有していない状態になります。彼らはリソースを所有していません。それは実現可能ではありません。有効 .これらのタイプについては、破壊的な移動のみを実装する必要があります。リソースのない状態は無効であるため、何もしないでください。

結論

移動安全性は便利な定義です。それを使用すると、オブジェクトの移動元状態を簡単に分類できます。オーバーヘッドなしで実装できる最も安全なレベルをサポートすることを選択するか、破壊的を選択して使用可能な移動フォーム状態を意図的に回避する必要があります。移動します。

Move Safety の概念は、これらの Stackoverflow の質問に対する答えを簡単に提供できます。さらに、独自の型のドキュメント化にも役立ちます。

このブログ投稿を書いているときに、デフォルトの構築に関していくつかのことに気付きました。フォローアップは、Move Semantics and Default Constructors – Rule of Six? にあります。