はじめに
C++11 から導入された emplace_back
は、コンテナに要素をその場で構築できる便利なメソッドです。
一方、それ以前から使われてきた push_back
とはどんな違いがあり、emplace_back
は完全に push_back
を代替できるのでしょうか?
本記事では、
-
emplace_back
が本当にpush_back
より常に速いのか -
両者がどんなケースに使えるのか
-
実装の違いと注意点
について、自分なりに整理した考えをまとめます。
push_back
と emplace_back
の違い
push_back
// C++20以降
constexpr void push_back(const T& value); // コピー
constexpr void push_back(T&& value); // ムーブ
push_back
は、すでに生成されたオブジェクトをコピーまたはムーブして格納する関数です。
emplace_back
// C++20以降
template<class... Args>
constexpr reference emplace_back(Args&&... args); // その場で構築
emplace_back
は、コンストラクタに渡す引数をそのまま受け取り、コンテナ内部で直接オブジェクトを構築します。そのため、中間オブジェクトなしに要素を追加できる点が特徴です。
これらの大きな違いは、push_back
が既に生成されたオブジェクト(T型)をコピーまたはムーブする一方で、emplace_back
は任意のコンストラクタ引数からその場でT型のインスタンスを構築できる点です。
LLVMにおける内部実装
以下はLLVM(libc++)におけるpush_back
内部の実装です。
if (this->__end_ != this->__end_cap()) {
__construct_one_at_end(__x); // 残り容量がある場合、__xをコピーまたはムーブ構築
} else {
__push_back_slow_path(__x); // 足りない場合、新規領域を確保して再配置
}
__construct_one_at_end
の中では、allocatorが提供する construct()
を使って、指定されたメモリにオブジェクトを構築しています。
また、allocatorが construct()
を持たない場合には単純に in-place new を呼び出す仕組みです。
__push_back_slow_path
容量が足りない場合は、__split_buffer
を使って新規メモリを確保し、既存要素をムーブまたはコピー後、新しい要素を追加してから内部バッファをスワップします。
これにより、強い例外保証が確保され、例外が投げられても元のベクタが壊れないように作られているのがポイントです。
emplace_back
の内部
非特化版 emplace_back
は以下のように、任意の引数から直接要素を構築します。
if (this->__end_ < this->__end_cap()) {
__construct_one_at_end(std::forward<Args>(args)...);
} else {
__emplace_back_slow_path(std::forward<Args>(args)...);
}
slow path も push_back
とほぼ同じで、新しいメモリ領域を確保してからその場で構築を行っています。
大きな違いは、__construct_one_at_end
に渡す引数が「T型そのものではなく、Tのコンストラクタ引数」である点です。
性能面で emplace_back
は常に有利か?
結論から言うと、必ずしも有利とは限りません。
✅ 有利なケース
-
要素が重たいコピーコンストラクタを持ち、かつムーブできない場合。
-
コンテナ内部で直接構築できると、一時オブジェクトが不要なので効率的。
反例:リテラルからの文字列構築
Arthur O'Dwyer の記事「Don't blindly prefer emplace_back to push_back」では、以下のPythonコードでベンチマークが取られていました。
import sys
print('#include <vector>')
print('#include <string>')
print('extern std::vector<std::string> v;')
for i in range(1000):
print(f'void test{i}() {{')
print(f' v.{sys.argv[1]}_back("{ "A" * i }");')
print('}')
これにより、例えば次のような呼び出しが生成されます。
v.push_back("A");
v.push_back("AA");
v.push_back("AAA");
// ... 省略 ...
v.emplace_back("A");
v.emplace_back("AA");
v.emplace_back("AAA");
// ... 省略 ...
結果
-
push_back
では、各呼び出しで単にstring(const char*)
が呼ばれます。 -
emplace_back
ではリテラル長ごとに1000通りの異なるテンプレート展開が行われ、コンパイル負荷と実行速度に影響が出ます。
ベンチマークではpush_back
版が1.0s、emplace_back
版が4.2sとなり、明らかにemplace_back
が遅いケースがあることが示されました。
結論:使い分けが重要
-
多くの場合、複雑なオブジェクトを直接構築できる
emplace_back
が有利。 -
ただし、単純に既存オブジェクトやリテラルから追加するだけの場合は
push_back
でも十分。 -
テンプレート展開によるコンパイル影響が気になる場面では、
push_back
を選ぶべきです。
まとめ
-
emplace_back
は中間オブジェクトなしに直接構築できるため、高コストなコピーが避けられる場面では有利です。 -
ただし、単純に既存オブジェクトやリテラルから追加するだけの場合、テンプレート展開やコード膨張が性能面でマイナスになる場合があります。
結局のところ、状況に応じて使い分けることが大切です。 両者にはそれぞれ利点と適用場面があるからこそ、標準ライブラリには二通りが用意されているのです。
※ 本記事は Zenn にも同時投稿しています。