- cpp17[meta cpp]
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
への変換は、上記の「一時オブジェクトの実体化は一般に可能な限り遅らせられる」ことより、一時オブジェクトを実体化しない。よって、次の例におけるprvalue
のT
型の戻り値は、呼び出し元の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()の引数初期化時、コピーは省略される
}
- vector[link ../../reference/vector/vector.md]
- cout[link ../../reference/iostream/cout.md]
最適化された場合
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型のコピーコンストラクタが必要
- コピー省略 - cppreference.com
- 値のコピー省略の保証について|teratail
- c++ - How does guaranteed copy elision work? - Stack Overflow
- 右辺値、左辺値などの細かい定義 - Qiita
- C++1z 値のコピー省略を保証 - Faith and Brave - C++で遊ぼう
- P0135R1 Wording for guaranteed copy elision through simplified value categories
- P0135R0 Guaranteed copy elision through simplified value categories
- Guaranteed Copy Elision