potisanのプログラミングメモ

プログラミング素人です。昔の自分を育ててくれたネット情報に少しでも貢献できるよう、情報を貯めていこうと思っています。Windows環境のC++やC#がメインです。

C++17 右辺値参照とRVO

次のコードのs3s4の逆アセンブリが予想と違いました。

#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;
}
  • 予想:s3s4はどちらもstd::wstringのムーブコンストラクタが呼び出される。
  • 実際:s3s4はどちらもムーブコンストラクタをスキップして一時オブジェクトまたは右辺値参照が直接代入される。

MSVCの実装を確認したところstd::wstringstd::basic_string)同士の+演算子は一様初期化を使用して一時オブジェクトを返しており(return {...};)、C++17からはRVO1(NRVO以外)の適用が強制でした。std::movestatic_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強制が特徴)である意識は必要かと思いました。右辺値参照を受け取るコンストラクタのうちムーブするものがムーブコンストラクタではなくて、右辺値参照を受け取るコンストラクタがムーブコンストラクタであるべきなのだと。

参考

蛇足

最初のコード、実際には以下のようにサフィックスを使うかなと思います。

#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);
}

  1. Return Value Optimization