web-dev-qa-db-ja.com

emplace_backの代わりにPush_backを使用するのはなぜですか?

C++ 11ベクトルには、新しい関数emplace_backがあります。コピーを回避するためにコンパイラの最適化に依存するPush_backとは異なり、emplace_backは完全転送を使用してコンストラクターに引数を直接送信し、インプレースでオブジェクトを作成します。 emplace_backPush_backができるすべてのことをしているように思えますが、時にはそれが良くなることもあります(しかし決して悪くはなりません)。

Push_backを使用する理由は何ですか?

207
David Stone

過去4年間、この質問についてかなり考えてきました。 Push_backemplace_backに関するほとんどの説明では全体像が見当たらないという結論に達しました。

昨年、C++ Nowで C++ 14の型推論 についてプレゼンテーションを行いました。 13:49からPush_backemplace_backについて話し始めましたが、その前にいくつかの裏付けとなる証拠を提供する有用な情報があります。

本当の主な違いは、暗黙的コンストラクタと明示的コンストラクタに関係しています。 Push_backまたはemplace_backに渡す引数が1つしかない場合を考えます。

std::vector<T> v;
v.Push_back(x);
v.emplace_back(x);

最適化コンパイラーがこれを手に入れた後、生成されたコードに関してこれら2つのステートメントの間に違いはありません。伝統的な知恵では、Push_backは一時オブジェクトを作成し、vに移動しますが、emplace_backは引数を転送し、コピーも移動もせずに直接配置します。これは、標準ライブラリに記述されたコードに基づいて真実かもしれませんが、最適化コンパイラの仕事はあなたが書いたコードを生成することであるという誤った仮定を作ります。最適化コンパイラの仕事は、プラットフォーム固有の最適化の専門家であり、保守性を気にせず、パフォーマンスだけを重視した場合に作成したコードを実際に生成することです。

これら2つのステートメントの実際の違いは、より強力なemplace_backがあらゆるタイプのコンストラクターを呼び出すのに対し、より慎重なPush_backは暗黙的なコンストラクターのみを呼び出すことです。暗黙のコンストラクタは安全であると想定されています。 UからTを暗黙的に構築できる場合、UTのすべての情報を損失なく保持できると言っています。 Tを渡すことはほとんどどんな状況でも安全であり、代わりにUにしたとしても誰も気にしないでしょう。暗黙的なコンストラクターの良い例は、std::uint32_tからstd::uint64_tへの変換です。暗黙の変換の悪い例は、doubleからstd::uint8_tです。

私たちはプログラミングに注意を払いたいです。強力な機能を使用したくないのは、機能が強力であればあるほど、誤った操作や予期しない操作を誤って簡単に実行できるためです。明示的なコンストラクターを呼び出す場合は、emplace_backのパワーが必要です。暗黙のコンストラクターのみを呼び出したい場合は、Push_backの安全性に固執してください。

std::vector<std::unique_ptr<T>> v;
T a;
v.emplace_back(std::addressof(a)); // compiles
v.Push_back(std::addressof(a)); // fails to compile

std::unique_ptr<T>には、T *からの明示的なコンストラクターがあります。 emplace_backは明示的なコンストラクターを呼び出すことができるため、非所有ポインターを渡すと問題なくコンパイルされます。ただし、vが範囲外になると、デストラクタはそのポインタでdeleteを呼び出そうとしますが、これは単なるスタックオブジェクトであるためnewによって割り当てられませんでした。これにより、未定義の動作が発生します。

これは単に発明されたコードではありません。これは私が遭遇した実際の生産上のバグでした。コードはstd::vector<T *>でしたが、コンテンツを所有していました。 C++ 11への移行の一環として、T *std::unique_ptr<T>に正しく変更して、ベクターがメモリを所有していることを示しました。ただし、2012年にこれらの変更を理解に基づかせていたので、「emplace_backはPush_backでできることをすべて実行するので、なぜPush_backを使用するのでしょうか?」と考えたので、Push_backemplace_back

代わりに、より安全なPush_backを使用するようにコードを残した場合、この長年のバグを即座にキャッチでき、C++ 11へのアップグレードの成功と見なされていました。代わりに、バグをマスクし、数か月後まで見つけられませんでした。

127
David Stone

Push_backは常に均一な初期化の使用を許可しますが、これは非常に好きです。例えば:

struct aggregate {
    int foo;
    int bar;
};

std::vector<aggregate> v;
v.Push_back({ 42, 121 });

一方、v.emplace_back({ 42, 121 });は機能しません。

114
Luc Danton

C++ 11以前のコンパイラとの後方互換性。

79
Mehrdad

Emplace_backの一部のライブラリ実装は、Visual Studio 2012、2013、および2015に同梱されているバージョンを含め、C++標準で指定されたとおりに動作しません。

既知のコンパイラのバグに対応するために、パラメータがイテレータまたは呼び出し後に無効になる他のオブジェクトを参照する場合は、usingstd::vector::Push_back()を使用します。

std::vector<int> v;
v.emplace_back(123);
v.emplace_back(v[0]); // Produces incorrect results in some compilers

あるコンパイラでは、vには期待される123と123の代わりに値123と21が含まれます。これは、emplace_backの2回目の呼び出しによりv[0]が無効になるサイズ変更が行われるためです。

上記のコードの実際の実装では、次のようにPush_back()の代わりにemplace_back()を使用します。

std::vector<int> v;
v.emplace_back(123);
v.Push_back(v[0]);

注:intのベクトルの使用は、デモンストレーション用です。動的に割り当てられたメンバー変数を含むはるかに複雑なクラスでこの問題を発見し、emplace_back()を呼び出すとハードクラッシュが発生しました。

67
Marc