C++ 11ベクトルには、新しい関数emplace_back
があります。コピーを回避するためにコンパイラの最適化に依存するPush_back
とは異なり、emplace_back
は完全転送を使用してコンストラクターに引数を直接送信し、インプレースでオブジェクトを作成します。 emplace_back
はPush_back
ができるすべてのことをしているように思えますが、時にはそれが良くなることもあります(しかし決して悪くはなりません)。
Push_back
を使用する理由は何ですか?
過去4年間、この質問についてかなり考えてきました。 Push_back
対emplace_back
に関するほとんどの説明では全体像が見当たらないという結論に達しました。
昨年、C++ Nowで C++ 14の型推論 についてプレゼンテーションを行いました。 13:49からPush_back
対emplace_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
を暗黙的に構築できる場合、U
はT
のすべての情報を損失なく保持できると言っています。 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_back
をemplace_back
。
代わりに、より安全なPush_back
を使用するようにコードを残した場合、この長年のバグを即座にキャッチでき、C++ 11へのアップグレードの成功と見なされていました。代わりに、バグをマスクし、数か月後まで見つけられませんでした。
Push_back
は常に均一な初期化の使用を許可しますが、これは非常に好きです。例えば:
struct aggregate {
int foo;
int bar;
};
std::vector<aggregate> v;
v.Push_back({ 42, 121 });
一方、v.emplace_back({ 42, 121 });
は機能しません。
C++ 11以前のコンパイラとの後方互換性。
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()
を呼び出すとハードクラッシュが発生しました。