次のコードのs3
、s4
の逆アセンブリが予想と違いました。
#include <string> int main() { std::wstring s1(L"abc"); std::wstring s2(L"def"); std::wstring s3(s1 + s2); // 一時オブジェクトで初期化 std::wstring s4(std::move(s1 + s2)); // 右辺値参照で初期化 return 0; }
- 予想:
s3
とs4
はどちらもstd::wstring
のムーブコンストラクタが呼び出される。 - 実際:
s3
とs4
はどちらもムーブコンストラクタをスキップして一時オブジェクトまたは右辺値参照が直接代入される。
MSVCの実装を確認したところstd::wstring
(std::basic_string
)同士の+
演算子は一様初期化を使用して一時オブジェクトを返しており(return {...};
)、C++17からはRVO1(NRVO以外)の適用が強制でした。std::move
もstatic_cast
の結果をそのまま返しており、RVOの対象になったと考えられます
RVOはムーブコンストラクタより効率的ですが、クラスが右辺値参照を受け取るコンストラクタで特殊な動作を実装しても呼び出されず、予想外の結果となります。例えば次の構造体は右辺値参照を受け取るコンストラクタでソースの保持する値+1
を初期値とする異様な動作をしますが、RVOが適用された場合は+1
されません。注意:C++17以前ではRVOが強制ではないため、コンパイラにより挙動が異なる可能性があります。
#include <utility> // 右辺値参照を受け取るコンストラクタで特殊な挙動をする構造体 struct rvo_test { int x; rvo_test(int init) { x = init; } rvo_test(rvo_test&& r) { x = r.x + 1; } }; rvo_test operator+ (const rvo_test& l, const rvo_test& r) { // 一様初期化により一時オブジェクトを返す。 // C++17以降では強制的にRVOが適用される。 return { l.x + r.x }; } int main() { rvo_test a = 1; rvo_test b = 2; // RVOによりabは+演算子で直接初期化される。 // 右辺値参照を受け取るコンストラクタは無視される。 rvo_test ab1(a + b); // ab1 = 3 // 右辺値参照を受け取るコンストラクタが呼び出される。 rvo_test ab2(std::move(a + b)); // ab2 = 4 return 0; }
右辺値参照の目的は右辺値(一時オブジェクト等)の束縛なので上記のコンストラクタがむしろ仕様の想定外ですが、C++17がムーブセマンティクスを前提とした言語(RVO強制が特徴)である意識は必要かと思いました。右辺値参照を受け取るコンストラクタのうちムーブするものがムーブコンストラクタではなくて、右辺値参照を受け取るコンストラクタがムーブコンストラクタであるべきなのだと。
参考
- 右辺値参照・ムーブセマンティクス - cpprefjp C++日本語リファレンス
- 一様初期化 - cpprefjp C++日本語リファレンス
- 値のコピー省略を保証 - cpprefjp C++日本語リファレンス
- Rvalue references - reference - cppreference
蛇足
最初のコード、実際には以下のようにサフィックスを使うかなと思います。
#include <string> using namespace std::string_literals; int main() { auto s1 = L"abc"s; auto s2 = L"def"s; auto s3 = s1 + s2; auto s4 = std::move(s1 + s2); }