C++
Elisionをコピー
サーチ…
コピーエリートの目的
標準には、オブジェクトを初期化するためにオブジェクトがコピーまたは移動される場所があります。コピーエリート(時にはリターン値の最適化と呼ばれる)は、ある特定の状況下で、コンパイラーがコピーや移動を避けることを許可された最適化です。
以下の関数を考えてみましょう:
std::string get_string()
{
return std::string("I am a string.");
}
標準の厳密な言い回しによると、この関数は一時的なstd::string
を初期化し、それを戻り値オブジェクトにコピー/移動して一時的に破棄します。標準が、コードがどのように解釈されるかは非常に明確です。
コピーエリートは、C ++コンパイラが一時的なコピーの作成とその後のコピー/破棄を無視できるようにするルールです。つまり、コンパイラは一時的に初期化式を取り、関数の戻り値を直接初期化することができます。これは明らかにパフォーマンスを節約します。
ただし、ユーザーには2つの目に見える影響があります。
型には呼び出されたコピー/移動コンストラクタが必要です。たとえコンパイラがコピー/移動をエリートしたとしても、その型はまだコピー/移動できなければなりません。
コピー/移動コンストラクタの副作用は、溶出が起こりうる状況では保証されません。次の点を考慮してください。
struct my_type
{
my_type() = default;
my_type(const my_type &) {std::cout <<"Copying\n";}
my_type(my_type &&) {std::cout <<"Moving\n";}
};
my_type func()
{
return my_type();
}
呼び出しfunc
は何をしますか?一時的な値はmy_type
で、 my_type
は移動可能なタイプなので、 "Copying"は決して印刷されません。それでは "動く"と印刷されますか?
コピーエリシエーションルールがなければ、常に「移動」を印刷する必要があります。しかし、コピー・エリジス・ルールが存在するため、ムーブ・コンストラクタは呼び出されることもあれば呼び出されないこともあります。実装に依存します。
したがって、コピー/移動コンストラクタの呼び出しに依存することはできません。
elisionは最適化であるため、コンパイラはすべてのケースでelisionをサポートしない可能性があります。また、コンパイラが特定のケースをエリートするかどうかにかかわらず、タイプは、実行されていない操作を引き続きサポートする必要があります。したがって、コピー構築が省略された場合でも、呼び出されなくても型はコピーコンストラクタを保持する必要があります。
保証されたコピーエリシジョン
通常、elisionは最適化です。ほとんどすべてのコンパイラは、最も簡単な場合にはコピー・エリジョンをサポートしますが、elisionを使用すると、依然としてユーザーに特定の負担がかかります。すなわち、コピー/ムーブされていないタイプは、コピー/ムーブされていなければならない 。
例えば:
std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
return std::lock_guard<std::mutex>(a_mutex);
}
これは、 a_mutex
があるシステムによってa_mutex
保持されているミューテックスであるが、外部ユーザがスコープロックを持つことを望む場合に便利です。
std::lock_guard
はコピーまたは移動できないため、これも正当ではありません。事実上すべてのC ++コンパイラがコピー/移動を削除するにもかかわらず、標準ではそのタイプの操作が利用可能である必要があります。
C ++まで17。
C ++ 17では、特定の式の意味を効果的に再定義してコピー/移動が起こらないようにするためにelisionを要求しています。上記のコードを考えてみましょう。
pre-C ++ 17の言葉では、そのコードは一時的なものを作成し、その一時的なものを使用して戻り値にコピー/移動しますが、一時的なコピーは省略できます。 C ++の文言の下では、それはまったく一時的なものではありません。
C ++ 17では、 prvalue式は 、式と同じ型のオブジェクトを初期化するために使用されると、一時的に生成されません。式は、そのオブジェクトを直接初期化します。戻り値と同じ型のprvalueを返す場合、型はコピー/移動コンストラクタを持つ必要はありません。したがって、C ++の17のルールの下では、上記のコードは動作します。
prvalueの型が初期化されている型と一致する場合、C ++言語は機能します。上記のget_lock
指定すると、コピー/移動は必要ありません。
std::lock_guard the_lock = get_lock();
get_lock
の結果は同じ型のオブジェクトを初期化するために使用されるprvalue式なので、コピーや移動は起こりません。その表現は決して一時的なものではありません。 the_lock
を直接初期化するために使用されます。 elide elideにはコピー/移動がないので、elionはありません。
したがって、「保証されたコピー・エリジョン」という用語は誤った名前であるが、これはC ++の標準化のために提案されているような機能の名称である 。それはまったく溶出を保証するものではありません。それはコピー/移動を完全に排除し 、C ++を再定義して、コピー/移動が一切行われないようにします。
この機能は、prvalue式を含む場合にのみ機能します。このように、これは通常のelisionルールを使用します:
std::mutex a_mutex;
std::lock_guard<std::mutex> get_lock()
{
std::lock_guard<std::mutex> my_lock(a_mutex);
//Do stuff
return my_lock;
}
これはコピーエリートの有効なケースですが、C ++ 17のルールではこの場合コピー/ムーブは排除されません。そのため、戻り値の初期化に使用するコピー/移動コンストラクタは、型にはまだ存在する必要があります。 lock_guard
はそうでないので、これはまだコンパイルエラーです。実装では、トリビュアコピー可能型のオブジェクトを渡したり返すときに、コピーを削除することを拒否することができます。これはレジスタ内でそのようなオブジェクトを動かすことを可能にするためであり、一部のABIは呼び出し規約でそれを強制することがあります。
struct trivially_copyable {
int a;
};
void foo (trivially_copyable a) {}
foo(trivially_copyable{}); //copy elision not mandated
戻り値elision
関数からprvalue式を返し、 prvalue式の型が関数の戻り値の型と同じ場合、prvalue temporaryからのコピーは削除できます。
std::string func()
{
return std::string("foo");
}
ほとんどすべてのコンパイラは、この場合、一時的な構成を削除します。
パラメータelision
関数に引数を渡し、引数が関数のパラメータ型のprvalue式であり、この型が参照でない場合、prvalueの構成は省略できます。
void func(std::string str) { ... }
func(std::string("foo"));
これは、一時的なstring
を作成し、それを関数パラメータstr
移動することを意味しstr
。 Copy elisionは、一時的な+移動を使用するのではなく、この式がstr
でオブジェクトを直接作成できるようにしstr
。
これは、コンストラクタがexplicit
宣言されている場合に便利な最適化です。例えば、 func("foo")
と書くことができstring
が、 string
はconst char*
からstring
変換する暗黙のコンストラクタがあるだけです。そのコンストラクタがexplicit
であった場合、 explicit
コンストラクタを呼び出すためexplicit
一時的なものを使用することが強制されます。 Copy elisionは、不要なコピー/移動をする必要がなくなります。
名前付き戻り値elision
lvalue式を関数から返すと、このlvalue:
- その関数にローカルな自動変数を表します。
return
後に破棄されます - 自動変数は関数パラメータではありません
- 変数の型は関数の戻り値の型と同じ型です
これらのすべてが該当する場合、左辺値からのコピー/移動は省略できます。
std::string func()
{
std::string str("foo");
//Do stuff
return str;
}
より複雑なケースはelisionに適格ですが、より複雑なケースでは、コンパイラが実際にそれを削除する可能性は低くなります。
std::string func()
{
std::string ret("foo");
if(some_condition)
{
return "bar";
}
return ret;
}
コンパイラはまだret
消すことができますが、そうする可能性は低くなります。
前述のように、値パラメータについてはelisationは許可されていません。
std::string func(std::string str)
{
str.assign("foo");
//Do stuff
return str; //No elision possible
}
初期化エリッションをコピーする
prvalue式を使用して変数を初期化し、その変数がprvalue式と同じ型を持つ場合、コピーは省略できます。
std::string str = std::string("foo");
コピー初期化はこれを効果的にstd::string str("foo");
変換しstd::string str("foo");
(わずかな違いがあります)。
これは戻り値でも機能します:
std::string func()
{
return std::string("foo");
}
std::string str = func();
copy elisionがなければ、 std::string
のmoveコンストラクタへの2回の呼び出しを引き起こします。 Copy elisionは、これが移動コンストラクタを1回または0回呼び出すことを許可し、ほとんどのコンパイラは後者を選択します。