💡

C++ の emplace_back は push_back を完全に置き換えられるか?

に公開

はじめに

C++11 から導入された emplace_back は、コンテナに要素をその場で構築できる便利なメソッドです。
一方、それ以前から使われてきた push_back とはどんな違いがあり、emplace_back は完全に push_back を代替できるのでしょうか?

本記事では、

  • emplace_back が本当に push_back より常に速いのか

  • 両者がどんなケースに使えるのか

  • 実装の違いと注意点

について、自分なりに整理した考えをまとめます。

push_backemplace_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は中間オブジェクトなしに直接構築できるため、高コストなコピーが避けられる場面では有利です。

  • ただし、単純に既存オブジェクトやリテラルから追加するだけの場合、テンプレート展開やコード膨張が性能面でマイナスになる場合があります。

結局のところ、状況に応じて使い分けることが大切です。 両者にはそれぞれ利点と適用場面があるからこそ、標準ライブラリには二通りが用意されているのです。

※ 本記事は Qiita にも同時投稿しています。

Discussion