コピーの省略と戻り値の最適化とは何ですか?


377

コピーエリクションとは何ですか?(名前付き)戻り値の最適化とは何ですか?彼らは何を意味しますか?

どのような状況で発生する可能性がありますか?制限とは何ですか?


1
コピー省略はそれを見る1つの方法です。オブジェクトの省略またはオブジェクトの融合(または混乱)は別のビューです。
curiousguy

このリンクは役に立ちました。
サブルシーカー

回答:


246

前書き

技術概要については、この回答にスキップしてください

コピーエリクションが発生する一般的なケースについては、この回答にスキップしてください

コピーの省略は、特定の状況で余分な(潜在的に高価な)コピーを防止するために、ほとんどのコンパイラーによって実装される最適化です。これにより、実際には値渡しまたは値渡しが可能になります(制限が適用されます)。

as-ifルールを回避する(ha!)最適化の唯一の形式です。オブジェクトのコピー/移動に副作用があっても、コピーの省略を適用できます

Wikipediaから抜粋した次の例:

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C();
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f();
}

コンパイラと設定に応じて、次の出力はすべて有効です

"こんにちは世界"
コピーが作成されました。
コピーが作成されました。


"こんにちは世界"
コピーが作成されました。


"こんにちは世界"

これは、作成できるオブジェクトの数が少なくなるため、特定の数のデストラクタが呼び出されることに依存することもできないことを意味します。コピー/移動コンストラクタまたはデストラクタの呼び出しに依存できないため、コピー/移動コンストラクタまたはデストラクタ内に重要なロジックを含めないでください。

コピーまたは移動コンストラクターへの呼び出しが省略された場合、そのコンストラクターは依然として存在し、アクセス可能でなければなりません。これにより、たとえば、オブジェクトにプライベートまたは削除されたコピー/移動コンストラクターがあるため、通常はコピーできないオブジェクトのコピーがコピー除外によって許可されなくなります。

C ++ 17:C ++ 17以降、オブジェクトが直接返される場合、コピーエリシオンが保証されます。

struct C {
  C() {}
  C(const C&) { std::cout << "A copy was made.\n"; }
};

C f() {
  return C(); //Definitely performs copy elision
}
C g() {
    C c;
    return c; //Maybe performs copy elision
}

int main() {
  std::cout << "Hello World!\n";
  C obj = f(); //Copy constructor isn't called
}

2
2番目の出力がいつ発生し、いつ3番目の出力が発生するのか説明してください。
zhangxaochen 2014年

3
@zhangxaochenは、コンパイラーがその方法で最適化することを決定する時期と方法。
Luchian Grigore 14年

10
@zhangxaochen、最初の出力:コピー1はreturnからtempへ、2はtempからobjへコピー。2番目は、上記のいずれかが最適化されたときで、おそらくreutnrコピーが省略されます。スリスの両方が省略されます
ビクター14年

2
うーん、でも私の意見では、これは私たちが信頼できる機能でなければなりません。それができなければ、現代のC ++での関数の実装方法に深刻な影響を与えるからです(RVOとstd :: move)。CppCon 2014のビデオを見ていると、最近のすべてのコンパイラーは常にRVOを実行しているという印象を受けました。さらに、どこかで最適化を行わずに、コンパイラーがそれを適用することを読んだことがあります。しかし、もちろん、それについてはよくわかりません。それが私が求めている理由です。
j00hi

8
@ j00hi:returnステートメントにmoveを記述しないでください。rvoが適用されていない場合、デフォルトで戻り値はデフォルトで移動されます。
MikeMB 2015年

96

標準参照

あまり技術的ではない見解と概要については、この回答にスキップしてください

コピーエリクションが発生する一般的なケースについては、この回答にスキップしてください

コピー省略は標準で次のように定義されています。

12.8クラスオブジェクトのコピーと移動[class.copy]

なので

31)特定の基準が満たされると、オブジェクトのコピー/移動コンストラクターおよび/またはデストラクタに副作用がある場合でも、実装はクラスオブジェクトのコピー/移動構成を省略できます。そのような場合、実装は、省略されたコピー/移動操作のソースとターゲットを同じオブジェクトを参照する2つの異なる方法として扱い、そのオブジェクトの破棄は、2つのオブジェクトが後であったときに遅くなります。最適化なしで破棄されました。123このコピー/移動操作の省略はコピー省略と呼ばれ、次の状況で許可されます(複数のコピーを削除するために組み合わせることができます):

—クラスの戻り値の型を持つ関数のreturnステートメントで、式が関数の戻り値の型と同じcvunqualified型を持つ(関数またはcatch-clauseパラメーター以外の)非揮発性自動オブジェクトの名前である場合、自動オブジェクトを関数の戻り値に直接作成することで、コピー/移動操作を省略できます。

— throw式で、オペランドが、最も内側の囲んでいるtryブロックの終わりを超えて拡張されない(関数またはcatch-clauseパラメーター以外の)不揮発性自動オブジェクトの名前である場合(存在する場合) 1)、自動オブジェクトを直接例外オブジェクトに作成することにより、オペランドから例外オブジェクト(15.1)へのコピー/移動操作を省略できます。

—参照(12.2)にバインドされていない一時クラスオブジェクトが同じcv非修飾タイプのクラスオブジェクトにコピー/移動される場合、一時オブジェクトを直接に作成することにより、コピー/移動操作を省略できます。省略されたコピー/移動のターゲット

—例外ハンドラーの例外宣言(条項15)が例外オブジェクト(15.1)と同じタイプ(cv-qualificationを除く)のオブジェクトを宣言する場合、コピー/移動操作は例外宣言を処理することで省略できます。 exception-declarationで宣言されたオブジェクトのコンストラクタとデストラクタの実行を除いて、プログラムの意味が変更されない場合は、例外オブジェクトのエイリアスとして。

123)2つではなく1つのオブジェクトのみが破棄され、1つのコピー/移動コンストラクターが実行されないため、構築されたオブジェクトごとに1つのオブジェクトが破棄されます。

与えられた例は:

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

と説明しました:

ここで、省略の基準を組み合わせて、クラスのコピーコンストラクターへの2つの呼び出しを削除できます。Thingローカル自動オブジェクトtを関数の戻り値のf() 一時オブジェクトにコピーすることと、その一時オブジェクトをオブジェクトにコピーすることt2です。事実上、ローカルオブジェクトの構築は、t グローバルオブジェクトを直接初期化すると見なすことができt2、そのオブジェクトの破棄はプログラムの終了時に発生します。Thingにmoveコンストラクターを追加しても同じ効果がありますが、それは一時オブジェクトからt2省略されるmove構築です。


1
それはC ++ 17標準からですか、それとも以前のバージョンからですか?
Nils

90

コピー省略の一般的な形式

技術概要については、この回答にスキップしてください

あまり技術的ではない見解と概要については、この回答にスキップしてください

(名前付き)戻り値の最適化は、コピー省略の一般的な形式です。これは、メソッドから値によって返されたオブジェクトのコピーが省略されている状況を指します。標準に示されている例は、オブジェクトに名前が付けられているため、名前付き戻り値の最適化を示しています。

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  Thing t;
  return t;
}
Thing t2 = f();

一時が返されると、通常の戻り値の最適化が行われます。

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
Thing f() {
  return Thing();
}
Thing t2 = f();

コピーの省略が行われる他の一般的な場所は、一時が値で渡される場合です

class Thing {
public:
  Thing();
  ~Thing();
  Thing(const Thing&);
};
void foo(Thing t);

foo(Thing());

または例外がスローされ、値によってキャッチされたとき:

struct Thing{
  Thing();
  Thing(const Thing&);
};

void foo() {
  Thing c;
  throw c;
}

int main() {
  try {
    foo();
  }
  catch(Thing c) {  
  }             
}

コピー省略の一般的な制限は次のとおりです。

  • 複数の戻り点
  • 条件付き初期化

ほとんどの商用グレードのコンパイラーは、コピー省略&(N)RVOをサポートします(最適化設定に依存)。


4
「一般的な制限」の箇条書きが少しだけ説明されているのを見てみたいと思います...これらの制限要因は何ですか?
2013年

@phonetagger私はmsdnの記事にリンクしました。
Luchian Grigore 2013年

54

コピー削除は、オブジェクトの不要なコピー/移動を削除するコンパイラー最適化手法です。

次の状況では、コンパイラーはコピー/移動操作を省略できるため、関連するコンストラクターを呼び出せません。

  1. NRVO(名前付き戻り値の最適化):関数が値によってクラス型を返し、returnステートメントの式が自動ストレージ期間(関数パラメーターではない)の不揮発性オブジェクトの名前である場合、コピー/移動最適化されていないコンパイラによって実行されるものは省略できます。その場合、戻り値は、関数の戻り値が移動またはコピーされるストレージに直接作成されます。
  2. RVO(戻り値の最適化):関数が単純なコンパイラーによって宛先に移動またはコピーされる名前のない一時オブジェクトを返す場合、1のようにコピーまたは移動を省略できます。
#include <iostream>  
using namespace std;

class ABC  
{  
public:   
    const char *a;  
    ABC()  
     { cout<<"Constructor"<<endl; }  
    ABC(const char *ptr)  
     { cout<<"Constructor"<<endl; }  
    ABC(ABC  &obj)  
     { cout<<"copy constructor"<<endl;}  
    ABC(ABC&& obj)  
    { cout<<"Move constructor"<<endl; }  
    ~ABC()  
    { cout<<"Destructor"<<endl; }  
};

ABC fun123()  
{ ABC obj; return obj; }  

ABC xyz123()  
{  return ABC(); }  

int main()  
{  
    ABC abc;  
    ABC obj1(fun123());//NRVO  
    ABC obj2(xyz123());//NRVO  
    ABC xyz = "Stack Overflow";//RVO  
    return 0;  
}

**Output without -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor    
Constructor  
Constructor  
Constructor  
Destructor  
Destructor  
Destructor  
Destructor  

**Output with -fno-elide-constructors**  
root@ajay-PC:/home/ajay/c++# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors    
root@ajay-PC:/home/ajay/c++# ./a.out   
Constructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Move constructor  
Destructor  
Constructor  
Move constructor  
Destructor  
Destructor  
Destructor  
Destructor  
Destructor  

コピーの省略が行われ、copy- / move-constructorが呼び出されない場合でも、それは存在し、アクセス可能でなければならず(最適化がまったく行われなかったかのように)、そうでない場合はプログラムの形式が正しくありません。

ソフトウェアの監視可能な動作に影響を及ぼさない場所でのみ、このようなコピーの省略を許可する必要があります。コピーの省略は、観察可能な副作用を許可する(つまり、排除する)最適化の唯一の形式です。例:

#include <iostream>     
int n = 0;    
class ABC     
{  public:  
 ABC(int) {}    
 ABC(const ABC& a) { ++n; } // the copy constructor has a visible side effect    
};                     // it modifies an object with static storage duration    

int main()   
{  
  ABC c1(21); // direct-initialization, calls C::C(42)  
  ABC c2 = ABC(21); // copy-initialization, calls C::C( C(42) )  

  std::cout << n << std::endl; // prints 0 if the copy was elided, 1 otherwise
  return 0;  
}

Output without -fno-elide-constructors  
root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp  
root@ajay-PC:/home/ayadav# ./a.out   
0

Output with -fno-elide-constructors  
root@ajay-PC:/home/ayadav# g++ -std=c++11 copy_elision.cpp -fno-elide-constructors  
root@ajay-PC:/home/ayadav# ./a.out   
1

GCCには、-fno-elide-constructorsコピーの省略を無効にするオプションがあります。コピーの省略を回避したい場合は、を使用してください-fno-elide-constructors

現在、最適化が有効になっている場合(および他のオプションが無効に設定されていない場合)、ほとんどすべてのコンパイラーがコピーの省略を提供しています。

結論

コピーが削除されるたびに、1つのコピーとそれに対応する1つのコピーの破棄が省略されるため、CPU時間を節約でき、1つのオブジェクトが作成されないため、スタックフレームのスペースを節約できます。


6
声明 ABC obj2(xyz123());はそれがNRVOまたはRVOですか?一時的な変数/オブジェクトと同じではありませんか ABC xyz = "Stack Overflow";//RVO
Asif Mushtaq '28

3
RVOをより具体的に示すために、コンパイラーが生成するアセンブリーを参照できます(コンパイラーのフラグ-fno-elide-constructorsを変更して、差分を確認します)。godbolt.org/g/Y2KcdH
Gab是好人
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.