CERT.EXPR.MOVED_FROM.USE
ムーブ元オブジェクトを使用しない
CERT.EXPR.MOVED_FROM.USE チェッカーは、C++ プログラム (C++11 以降) におけるムーブ後使用に関する欠陥を検出します。オブジェクトに対して std::move が適用され、その結果が消費される (ムーブコンストラクターまたはムーブ代入演算子に渡される) と、ソースオブジェクトは有効ではあるものの未指定の状態になります。ムーブ元オブジェクトをその後使用する (値の読み取り、再初期化を行わないメンバー関数の呼び出し、消費関数への引き渡しなど) と、未指定の値に依存することになるため、未指定または未定義の挙動が発生します。
脆弱性とリスク
C++ 規格では、ムーブ元のオブジェクトが有効ではあるものの未指定の状態であることのみが保証されています。つまり、破棄や再代入を行っても安全ですが、その値は未指定となります。ムーブ元の std::string にアクセスすると、空の文字列やその他の値が返され、暗黙に出力に破損が生じる可能性があります。ムーブ元の std::vector にアクセスすると、通常は空であり、範囲ループやインデックスベースのアクセスにおいて、要素が暗黙に処理されない、あるいは範囲外へのアクセスが行われる原因となります。ムーブ元オブジェクトを、有効な値を想定している関数に渡すと、その未指定の状態が暗黙に呼び出しチェーンのさらに先へと伝播します。マルチステップのパイプラインでは、欠陥箇所がムーブ箇所から遠く離れていることが多く、ツールを使用せずに診断することは困難です。
軽減と防止
オブジェクトを std::move に渡した後は、そのオブジェクトに対して読み取りを行ったり、非自明なメソッドを呼び出したりしないでください。変数を再利用する必要がある場合は、次に使用する前に新しい値を代入します。operator= を含むいかなる代入も、ムーブ元の状態を再初期化します。変数のスコープを制限し、変数宣言の直後にムーブを実行するようにして、意図せぬ後続の使用が行われないようにしてください。所有権を移すと同時に、ソースを既知の値にリセットする必要がある場合は、std::exchange を使用してください。
脆弱コード例 1: ムーブ元オブジェクトに対してメソッドを呼び出す
#include <string>
#include <utility>
void f() {
std::string s = "hello";
std::string t = std::move(s); // s enters moved-from state
std::size_t n = s.size(); // CERT.EXPR.MOVED_FROM.USE: 's' used after move
}
std::move(s) が t のムーブコンストラクターによって消費された後、s はムーブ元の状態になります。s.size() を呼び出すと、未指定の値が読み取られます。実際には、ほとんどの std::string の実装では、ムーブ元の文字列が空になりますが、規格でこれが保証されているわけではありません。
脆弱コード例 2: ムーブ元オブジェクトが free 関数に渡される
#include <string>
#include <utility>
void consume(std::string s);
void g() {
std::string s = "hello";
consume(std::move(s)); // s enters moved-from state
consume(s); // CERT.EXPR.MOVED_FROM.USE: 's' used after move
}
1 回目の呼び出しでは、文字列データの所有権が移管されます。2 回目の呼び出しでは、ムーブ元の s からコピーされますが、この値は未指定です。
脆弱コード例 3: スマートポインターではないオブジェクトの二重ムーブ
#include <string>
#include <utility>
void h() {
std::string s = "hello";
std::string t = std::move(s); // s enters moved-from state
std::string u = std::move(s); // CERT.EXPR.MOVED_FROM.USE: 's' used after move
// u contains an unspecified value — silent logic error
}
スマートポインター (ムーブ元の状態は常に nullptr である) とは異なり、std::string やその他の標準ライブラリのほとんどの型では、ムーブ元の値は未指定となります。ムーブ元の s を 2 回目の std::move に渡すと、その未指定の値が読み取られます。
修正コード例 1: ムーブ後にオブジェクトを使用しない
#include <string>
#include <utility>
void consume(std::string s);
void f() {
std::string s = "hello";
consume(std::move(s)); // s is not used after this point — compliant
}
修正コード例 2: 再利用前に再初期化する
#include <string>
#include <utility>
void f() {
std::string s = "hello";
std::string t = std::move(s); // s enters moved-from state
s = "world"; // reinitialize: assignment restores s
std::size_t n = s.size(); // OK: s has a well-defined value again
}