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

履歴 編集

値のコピー省略を保証(C++17)

概要

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を参照