C++ 11 の rvalueリファレンスと swap
今までの swap
今までの swap の実装は次のような感じでしょうか
template<typename T> inline void swap( T& lhs, T& rhs ){ T temp = lhs; lhs = rhs; rhs = temp; }
コピーが3回呼ばれます
cfoo::cfoo( const cfoo& ) cfoo::operator=( const cfoo& ) cfoo::operator=( const cfoo& ) cfoo::~cfoo
当たり前ですね
ただ、rvalueリファレンスがある今はもう当たり前ではなくなりました
rvalueリファレンスと swap
上のコードを見て最後のコピーが勿体ないということにすぐ気が付くでしょうか
コピーの後 temp はもう寿命ですね
寿命が来たオブジェクトは後は捨ててしまうだけですが、この「後は捨ててしまうだけ」という情報は今や大きな情報です
「後は捨ててしまうだけ」というのはそのコピーを move にできるという合図です
template<typename T> inline void swap( T& lhs, T& rhs ){ T temp( lhs ); lhs = rhs; rhs = std::move( temp ); }
コピーを一つ節約できました
cfoo::cfoo( const cfoo& ) cfoo::operator=( const cfoo& ) cfoo::operator=( cfoo&& ) cfoo::~cfoo
捨ててしまうものをただそのまま捨ててしまうというのは今や勿体ないことなのです
もっとmove
上記のコードは実はもっと move できます
どこでしょうか
次の行に注目してみましょう
T temp( lhs );
ここでは temp に lhs の中身をコピーしていますね
ということはこの時点で同じ中身のものが temp と lhs と2つあることになります
コピーしているのですから当たり前のことですが、無駄ですね
しかも次の行では lhs に rhs を上書きしています
lhs = rhs;
つまりコピーを作り終わった後の lhs はもう不要なのです
「もう不要」ということはこれもこのコピーを move にできるという合図です
T temp( std::move( lhs ));
これでもう一つコピーを節約することができました
cfoo::cfoo( cfoo&& ) cfoo::operator=( const cfoo& ) cfoo::operator=( cfoo&& ) cfoo::~cfoo
次の行はどうでしょうか
lhs = rhs;
ここも lhs に rhs の中身をコピーした時点で同じ中身のものが2つできてしまっています
しかもやはり次の行では rhs に temp を上書きしています
rhs = std::move( temp );
ということはここもやはり rhs はもう不要ですね
ここも move にできます
lhs = std::move( rhs );
つまり swap は今や次のように書けるようになったということです
template<typename T> inline void swap( T& lhs, T& rhs ){ T temp( std::move( lhs )); lhs = std::move( rhs ); rhs = std::move( temp ); }
3回のコピーを全部ムーブに置き換えることができました
cfoo::cfoo( cfoo&& ) cfoo::operator=( cfoo&& ) cfoo::operator=( cfoo&& ) cfoo::~cfoo
C++ 11 時代ではこの「コピーした後にコピー元が不要になるかどうか。」という点にも気を配れるようになれるといいと思います
どのようなものが不要か
コピーした後にコピー元が不要になるケースというのは「コピー以降使われない」か「コピー後最初に使われるのが書き込み先である場合」かのどちらかの場合でしょう
書き込み先というのは式の左辺や関数の出力引数のことです
全部を上書きされることが分かっているものはもう不要でしょう
一部値が残っていることを期待されるようなものは不要ではありません
不要でないものを move してはいけません
moveした後の変数に触ってもいいのか
以前にもお話ししましたが、やはり move した後の変数に触ってもいいのかということを気にする人が結構います
もう一度言いますが move は特別な何かというわけではなくただの破壊的なコピーです
move 後に触ってはいけない理由はありません
しかしもう触らないから move するのであってまだ触りたいなら move してはいけません
ただ、ここでも lhs や rhs を move した後に代入しています
触っているのに move していいのかと思うかもしれません
触るというのは実は値を読み出す readアクセスという意味でした
ですから言い換えると、もうその変数から値を読み出さないから move するのであってまだ値を読み出したいなら move してはいけません。となるでしょうか
別に move後の変数から値を読み出したからといって例外が発生したりするようなものでもありませんが、まだ値を読み出したいならそれはまだ不要でないということです
不要でないものを move してはいけません