• Class / Function / Type

      std::
    • Header file

      <>
    • Other / All

    最終更新日時(UTC):
    が更新

    履歴 編集

    値のコピー省略を保証 [P0135R1]

    このページはC++17に採用された言語機能の変更を解説しています。

    のちのC++規格でさらに変更される場合があるため関連項目を参照してください。

    概要

    C++11で右辺値参照を導入するときに規定された「値カテゴリー (value category)」の仕様(C++17で更新)を利用し、prvalue[注1]というカテゴリーの値を、オブジェクトの初期化のために使用する場合に、コピーが省略される。

    仕様

    まず、prvalueなどの値カテゴリー (value category)については、n4659[basic.lval]/1に定義されている。

    また、[conv.rval]/1 に、次のような記述がある:

    Temporary materialization conversion

    T型のprvalueは、T型のxvalueに変換できる。この変換では、一時オブジェクトを結果オブジェクトとしてprvalueを評価することによって、prvalueからT型の一時オブジェクトを初期化し、その一時オブジェクトを表すxvalueを生成する。T完全型でなければならない。 [注:Tがクラス型(またはその配列)の場合、アクセス可能な削除されていないデストラクタが必要である。] 例:

    struct X { int n; }
    int k = X().n; // ok, X() prvalue は xvalue に変換される
    

    つまり、prvalueは一時オブジェクトではない(C++17以降)。次のときにはじめて結果オブジェクトとして一時オブジェクトを使用して評価される。

    [class.temporary]/2 より:

    不必要な一時オブジェクトの作成を避けるために、一時オブジェクトの実体化は一般に可能な限り遅らせる。 注:一時オブジェクトは次のとき実体化されている。

    • 参照をprvalueにバインドするとき
    • クラスprvalueでメンバアクセスを実行するとき
    • 配列からポインタへの変換を実行するとき、または配列prvalueをサブスクライブするとき
    • braced-init-listからstd::initializer_list<T>型のオブジェクトを初期化するとき
    • 特定の未評価のオペランド、およびprvalueが廃棄値式(discarded-value expression)として現れる場合

    これより、上の例は「クラスprvalueでメンバアクセスを実行するとき」にあたり、xvalueとして一時オブジェクトを生成している。

    また、prvalueからprvalueへの変換は、上記の「一時オブジェクトの実体化は一般に可能な限り遅らせられる」ことより、一時オブジェクトを実体化しない。よって、次の例におけるprvalueT型の戻り値は、呼び出し元のtを直接初期化する。

    T Func() {return T();} 
    T t = Func(); // 直接初期化
    

    コピー省略 - cppreference.comより引用した。

    #include <iostream>
    #include <vector>
    
    struct Noisy {
      Noisy() { std::cout << "constructed\n"; }
      Noisy(const Noisy&) { std::cout << "copy-constructed\n"; }
      Noisy(Noisy&&) { std::cout << "move-constructed\n"; }
      ~Noisy() { std::cout << "destructed\n"; }
    };
    
    std::vector<Noisy> f() {
      std::vector<Noisy> v = std::vector<Noisy>(3); // v 初期化時、コピーは省略される
      return v; // NRVO は、C++17でも保証されない
    }             // 最適化されない場合、ムーブコンストラクタがよばれる
    
    void g(std::vector<Noisy> arg) {
      std::cout << "arg.size() = " << arg.size() << '\n';
    }
    
    int main() {
      std::vector<Noisy> v = f(); // v 初期化時、コピーは省略される
      g(f());                     // g()の引数初期化時、コピーは省略される
    }
    

    出力例

    最適化された場合

    constructed
    constructed
    constructed
    constructed
    constructed
    constructed
    arg.size() = 3
    destructed
    destructed
    destructed
    destructed
    destructed
    destructed
    

    この機能が必要になった背景・経緯

    関数の戻り値のコピーを発生させない手法として、RVO (Return Value Optimization) やNRVO (Named Return Value Optimization) といった最適化があった(注:RVOは、NRVOと区別せずに使われることがある)。

    // RVOの最適化が動作した場合
    struct Foo {};
    
    Foo foo()
    {
      return Foo();
    }
    
    Foo x = foo(); // Foo型のコピーコンストラクタが動作することなくxが初期化される
    

    // NRVOの最適化が動作した場合
    struct Foo { int value = 0; };
    
    Foo foo()
    {
      Foo y;
      y.value = 42;
      return y;
    }
    
    Foo x = foo(); // Foo型のコピーコンストラクタが動作することなくxが初期化される
    

    しかし、これらの最適化はコンパイラに対して許可された動作であって、そのように最適化されることが保証されるものではない。そのため、実際には(N)RVOによってコピーは起こらないけどコピーコンストラクタは用意しなければならない、といったことになった。

    C++17では、このようなコピー省略を保証する仕組みが導入される。そのため、オブジェクトの初期化のために使用するprvalueは、コピーもムーブもできない型であっても、関数の戻り値として返せるようになる。つまり、NRVOと区別してRVOの場合は、コピーコンストラクタを用意しなくてもよくなった。NRVOは依然として保証がないことに注意。

    // C++17
    struct Foo {
      // Fooはコピーもムーブもできない
      Foo() = default;
      Foo(const Foo&) = delete;
      Foo(Foo&&) = delete;
    };
    
    Foo foo()
    {
      return Foo();
    }
    
    Foo y = foo(); // OK
    

    // C++17
    struct Foo {
      // Fooはコピーもムーブもできない
      Foo() = default;
      Foo(const Foo&) = delete;
      Foo(Foo&&) = delete;
    };
    
    Foo foo()
    {
      Foo y;
      return y;
    }
    
    Foo x = foo(); // error Foo型のコピーコンストラクタが必要
    

    参照

    注釈

    1. ^ 右辺値、左辺値などの細かい定義 - Qiitaを参照