C++コンピュータ プログラミングでは、コピー省略とは、オブジェクトの不要なコピーを排除するコンパイラ最適化手法を指します。
C++ 言語標準では、一般に実装によるあらゆる最適化が許可されていますが、その結果得られるプログラムの観察可能な動作が、つまりプログラムが標準で要求されているとおりに正確に実行されたと仮定した場合と同じである場合に限られます。さらに、標準では、コピーを省略できる状況もいくつか説明されており、最も一般的なのは戻り値の最適化です (下記 を参照)。C ++ 標準で説明されているもう 1 つの広く実装されている最適化は、クラス型の一時オブジェクトが同じ型のオブジェクトにコピーされる場合です。 [ 1 ] [ 2 ]結果として、コピー初期化は通常、パフォーマンスの点では直接初期化と同等ですが、意味的には同等ではありません。コピー初期化では、アクセス可能なコピー コンストラクターが依然として必要です。[ 3 ]最適化は、参照にバインドされている一時オブジェクトには適用できません。
stdをインポートします。// グローバルな可変変数int n = 0 ;struct MyStruct { explicit MyStruct ([[ maybe_unused ]] int x ) {} MyStruct ([[ maybe_unused ]] const MyStruct & x ) { ++ n ; // コピー コンストラクターには目に見える副作用があります// 静的ストレージ期間を持つオブジェクトを変更します} };int main () { MyStruct c1 ( 42 ); // 直接初期化、MyStruct::MyStruct(int) を呼び出すMyStruct c2 = MyStruct ( 42 ); // コピー初期化、MyStruct::MyStruct(const MyStruct&) を呼び出すstd :: println ( "{}" , n ); // コピーが省略された場合は0を、そうでない場合は1を出力します}標準規格によれば、同様の最適化はスローおよびキャッチされるオブジェクトにも適用できる可能性があるが[ 4 ] [ 5 ]、最適化がスローされたオブジェクトから例外オブジェクトへのコピーと、例外オブジェクトからcatch節の例外宣言で宣言されたオブジェクトへのコピーの両方に適用されるかどうかは不明である。また、この最適化が一時オブジェクトのみに適用されるのか、それとも名前付きオブジェクトにも適用されるのかについても不明である。[ 6 ]以下のソースコードがあるとする。
stdをインポートします。構造体MyStruct { MyStruct () = default ; MyStruct ([[ maybe_unused ]] const MyStruct & x ) { std :: println ( "Hello World!" ); } };void f () { MyStruct c ; throw c ; // 名前付きオブジェクト c を例外オブジェクトにコピーします。// このコピーが省略できるかどうかは不明です。}int main () { try { f (); } catch ( MyStruct c ) { // 例外宣言で例外オブジェクトを一時的にコピーします。// このコピーが省略できるかどうかも不明です。} }したがって、準拠コンパイラは「Hello World!」を2回出力するプログラムを生成する必要があります。C++標準のC++11リビジョンでは、これらの問題が解決され、基本的に名前付きオブジェクトから例外オブジェクトへのコピーと、例外ハンドラで宣言されたオブジェクトへのコピーの両方を省略できるようになりました。[ 6 ]
GCCにはコピー省略を無効にするオプションがあります-fno-elide-constructors。このオプションは、戻り値の最適化やコピー省略を伴うその他の最適化の効果を観察(または観察しない)するのに便利です。この重要な最適化を無効にすることは、一般的に推奨されません。
C++17では「コピー省略の保証」が提供されており、prvalueは必要になるまで実体化されず、最終的な宛先のストレージに直接構築されます。[ 7 ]
C++プログラミング言語において、戻り値最適化(RVO)は、関数の戻り値を保持するために作成された一時オブジェクトを削除するコンパイラ最適化である。 [ 8 ] RVOは、C++標準によって、結果として得られるプログラムの観測可能な動作を変更することが許可されている。[ 9 ]
一般的に、C++標準では、結果として得られる実行ファイルが、標準のすべての要件が満たされているかのように(つまり、そのように装っているかのように)同じ動作を示す限り、コンパイラが任意の最適化を実行することが許可されています。これは一般に「 as-ifルール」と呼ばれています。[ 10 ] [ 2 ]戻り値最適化という用語は、C++標準の特別な条項を指し、「as-if」ルールよりもさらに踏み込んだものです。実装は、コピーコンストラクタに副作用があっても、return文から生じるコピー操作を省略することができます。[ 1 ] [ 2 ]
以下の例は、コピーコンストラクタに目に見える副作用(テキストの出力)がある場合でも、実装によって作成されるコピーの一方または両方が削除される可能性があるシナリオを示しています。[ 1 ] [ 2 ]削除される可能性がある最初のコピーは、関数の戻り値MyStructにコピーされる可能性のある名前のない一時オブジェクトです。削除される可能性がある2番目のコピーは、 toによって返される一時オブジェクトのコピーです。 ffobj
stdをインポートします。struct MyStruct { MyStruct () = default ; MyStruct ([[ maybe_unused ]] const MyStruct & x ) { std :: println ( "コピーが作成されました。" ); } };MyStruct f () { MyStruct ()を返します; }int main () { std :: println ( "Hello World!" ); MyStruct obj = f (); }コンパイラとそのコンパイラの設定に応じて、結果のプログラムは次のいずれかの出力を表示する場合があります。
「こんにちは世界」 コピーが作成されました。 コピーが作成されました。
「こんにちは世界」 コピーが作成されました。
「こんにちは世界」
関数から組み込み型のオブジェクトを返す場合、そのオブジェクトは通常CPUレジスタに収まるため、オーバーヘッドはほとんど、あるいは全く発生しません。クラス型のより大きなオブジェクトを返す場合、メモリ上の位置から別の位置へのコピーにコストがかかる可能性があります。これを回避するため、実装では呼び出し元のスタックフレームに隠しオブジェクトを作成し、そのオブジェクトのアドレスを関数に渡すことができます。関数の戻り値は、この隠しオブジェクトにコピーされます。[ 11 ]したがって、次のようなコードでは、
構造体Data { charバイト[ 16 ]; };Data f () { Data result ; // 結果を生成するreturn result ; }int main () {データd = f (); }次のようなコードを生成する可能性があります:
構造体Data { charバイト[ 16 ]; };Data * f ( Data * _hiddenAddress ) { Data result ; // 結果を隠しオブジェクトにコピー* _hiddenAddress = result ; return _hiddenAddress ; }int main () { Data _hidden ; // 隠しオブジェクトを作成Data d = * f ( & _hidden ); // 結果を d にコピー}これにより、Dataオブジェクトが 2 回コピーされます。
C++の進化の初期段階では、関数からクラス型のオブジェクトを効率的に返すことができないという言語の弱点が問題視されていました。 [ 12 ] 1991年頃、ウォルター・ブライトはコピーを最小限に抑える手法を実装し、関数内の隠しオブジェクトと名前付きオブジェクトを、結果を保持するために使用されるオブジェクトに効果的に置き換えました。[ 13 ]
構造体Data { charバイト[ 16 ]; };void f ( Data * p ) { // *p に直接結果を生成する}int main () {データd ; f ( & d ); }ブライトはこの最適化をZortech C++コンパイラに実装した。[ 12 ]この手法は後に「名前付き戻り値最適化」(NRVO)と名付けられ、名前付きオブジェクトのコピーが省略されるという事実に由来している。[ 13 ]
戻り値の最適化はほとんどのコンパイラでサポートされています。[ 8 ] [ 14 ] [ 15 ] ただし、コンパイラが最適化を実行できない状況もあります。よくあるケースの1つは、関数が実行パスに応じて異なる名前付きオブジェクトを返す場合です。[ 11 ] [ 14 ] [ 16 ]
stdをインポートします。std :: stringを使用します。string f ( bool cond = false ) { string first = "first" ; string second = "second" ; // 関数は引数に応じて2つの名前付きオブジェクトのいずれかを返す場合があります。RVOは適用されない可能性があります。return cond ? first : second ; }int main () {文字列結果= f (); }