【C++】いまさらだけどvectorのpush_back()とemplace_back()を比較する
std::vectorに要素を追加する方法にはpush_back()とemplace_back()の2つがあります。「何となくemplace_back()の方が速い」というイメージを持っている人は多いと思いますが、実際に何が違うのか、どのくらい差が出るのかを、コードと計測結果を交えて整理します。私の環境下では意外な結果が出ました。
検証環境:
- CPU: Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz
- メモリ: 16GB
- OS: Arch Linux
- g++: version 16.2.1
- clang++: version 22.1.8
それぞれの定義
まずはcpprefjpベースでの定義を確認します。
push_back(const T& x)/push_back(T&& x)
すでに構築済みのT型オブジェクト(またはそれに変換可能な値)を受け取り、それをvectorの末尾にコピーまたはムーブして格納する。emplace_back(Args&&... args)
コンストラクタへ渡す引数そのものを受け取り、vectorが確保したメモリ上でTオブジェクトを直接構築(placement new)する。可変長テンプレートによって任意個の引数を完全転送(perfect forwarding)する。
ポイントは、push_backが「完成品を受け取って配置する」のに対し、emplace_backは「材料(コンストラクタ引数)を受け取ってその場で作る」という点です。この違いが、後述する動作・パフォーマンスの差の根本原因になります。
それぞれの動作
動作確認用のクラスの実装
コンストラクタ・コピーコンストラクタ・ムーブコンストラクタ・デストラクタそれぞれにログを仕込んだクラスを用意し、どの関数が何回呼ばれるかを可視化します。
struct Widget
{
int id;
std::string name;
// 通常のコンストラクタ
Widget(int id_, std::string name_) : id(id_), name(std::move(name_)) {
std::cout << " [Constructor] id=" << id << " name=" << name << "\n";
}
// コピーコンストラクタ
Widget(const Widget& other) : id(other.id), name(other.name) {
std::cout << " [Copy Constructor] id=" << id << " name=" << name << "\n";
}
// ムーブコンストラクタ
Widget(Widget&& other) noexcept : id(other.id), name(std::move(other.name)) {
std::cout << " [Move Constructor] id=" << id << " name=" << name << "\n";
}
// デストラクタ
~Widget() {
std::cout << " [Destructor] id=" << id << " name=" << name << "\n";
}
};reserve()で事前に容量を確保し、再配置(reallocation)によるノイズを排除した上で検証します。
push_back() / emplace_back() したときの動作
std::vector<Widget> v;
v.reserve(10);
std::cout << "=== push_back(左辺値) ===\n";
Widget w1(1, "alpha");
v.push_back(w1);
std::cout << "\n=== push_back(右辺値/一時オブジェクト) ===\n";
v.push_back(Widget(2, "beta"));
std::cout << "\n=== emplace_back(引数を直接渡す) ===\n";
v.emplace_back(3, "gamma");実行結果は次の通りです。
=== push_back(左辺値) ===
[Constructor] id=1 name=alpha
[Copy Constructor] id=1 name=alpha
=== push_back(右辺値/一時オブジェクト) ===
[Constructor] id=2 name=beta
[Move Constructor] id=2 name=beta
[Destructor] id=2 name=
=== emplace_back(引数を直接渡す) ===
[Constructor] id=3 name=gammaそれぞれの内訳はこうなります。
| 呼び出し方 | 内部で起きていること |
|---|---|
push_back(w1)(左辺値) | Widgetをコピー構築。元のw1はそのまま残る。 |
push_back(Widget(2, "beta"))(右辺値) | 一時オブジェクトをコンストラクタで構築 → vector内へムーブ構築 → 一時オブジェクトを破棄。合計3回の関数呼び出し。 |
emplace_back(3, "gamma") | vectorの生メモリ上にコンストラクタで直接構築。コピーもムーブも発生しない。 |
つまり emplace_back()は「一時オブジェクトを作ってから移動する」という中間工程を丸ごと省略できるのが最大の特徴です。
{}(波括弧初期化) を渡した場合の挙動差
ここは見落とされがちですが重要な違いです。push_back()は引数を1つ受け取る関数なので、暗黙変換によって波括弧初期化リストをそのまま渡せます。
std::vector<Widget> v;
v.push_back({4, "delta"}); // OK: {4, "delta"} から一時Widgetへ暗黙変換されてpush_backされる実行結果:
=== push_back({...}) ===
[Constructor] id=4 name=delta
[Move Constructor]一方 emplace_back()はArgs&&...という可変長テンプレート引数を取るため、{4, "delta"}のような波括弧初期化リストからは型を推論できず、コンパイルエラーになります。
std::vector<Widget> v;
v.emplace_back({4, "delta"}); // コンパイルエラー実際にg++でコンパイルすると次のエラーが出ます。
error: no matching function for call to 'std::vector<Widget>::emplace_back(<brace-enclosed initializer list>)'
v.emplace_back({4, "delta"});
candidate expects 0 arguments, 1 providedemplace_back()で同じことをしたい場合は、波括弧を外して引数をそのまま渡す必要があります。
v.emplace_back(4, "delta"); // OKこの違いは、既存コードを push_back({...}) → emplace_back({...})に単純置換すると壊れる典型的な罠なので覚えておくとよいです。
動作時間の比較
std::stringメンバを持つWidgetを200万回vectorへ追加する処理を、g++とclang++の-O2最適化ありでベンチマークしました(reserve()済みで再配置は発生しない前提)。数値は10回計測した際の平均値と標準偏差です。
実際に使用したコードは最後に掲載しましたので、そちらをご覧ください。
オブジェクトの場合
| 呼び出し方 | g++所要時間 | clang++所要時間 | 内部動作 |
|---|---|---|---|
push_back(既存の左辺値 w) | 100.5±4.5 ms | 100.8±5.12 ms | 毎回コピーコンストラクタが発生(もっとも遅い) |
push_back(Widget(i, name))(一時オブジェクト) | 65.2±2.39 ms | 60.4±0.84 ms | 構築 + ムーブ + 破棄 |
emplace_back(i, name) | 81.9±9.11 ms | 65.5±0.53 ms | その場で直接構築のみ |
左辺値をコピーするケースは、std::stringの内部バッファを丸ごと複製するため明確に遅くなります。一方、右辺値(一時オブジェクト)のpush_back()とemplace_back()の比較は意外な結果が出ました。-O2環境では、これは std::stringのムーブがポインタの付け替え程度で非常に軽いため、「構築→ムーブ→破棄」という余分な工程があっても、その差が誤差レベルに収まってしまうと予想していたのですが、emplace_back()のほうが遅かったです。ただし、これはg++の生成するコードのせいかもしれません。clang++でコンパイルした場合、両者にはほとんど差がありませんでした。
このことから「emplace_back()は常に速い」というのは正確ではなく、正しく言うなら「emplace_back()は、ムーブコストが高いオブジェクトほど恩恵が大きい」 という理解が実態に近いです。
値の場合
intのようなプリミティブ型をvectorへ追加するケースです。
| 呼び出し方 | g++所要時間 | clang++所要時間 |
|---|---|---|
push_back(i) | 0±0 ms | 0±0 ms |
emplace_back(i) | 0±0 ms | 0±0 ms |
intはコピーもムーブも「4バイトの値渡し」でしかないため、push_back()とemplace_back()の間に実質的な差はありません。int/double/ポインタのようなトリビアルにコピー可能な型では、どちらを使ってもパフォーマンス上の違いはないと考えてよいです。
{}を渡した場合
| 呼び出し方 | g++所要時間 | clang++所要時間 |
|---|---|---|
push_back({i, name}) | 62.6±0.84 ms | 58.9±0.99 ms |
emplace_back(i, name)(同等の処理) | 81.0±9.18 ms | 68.2±7.33 ms |
前述の通り emplace_back({i, name})はそもそもコンパイルできないため、同じ結果を得るには emplace_back(i, name)のように引数を展開する必要があります。計測上はpush_back({...})の方が速いという結果でした。これも意外でした。
少なくとも「{}を使う場面ではemplace_back()が必ず有利」とは言えない、という点は押さえておくべきです。
まとめ
- 定義:
push_back()は完成したオブジェクトを配置し、emplace_back()はコンストラクタ引数からその場でオブジェクトを構築する。 - 動作:
push_back(左辺値)はコピー、push_back(右辺値)は構築+ムーブ+破棄、emplace_backは構築のみ。 {}の罠:push_back({...})は暗黙変換で動くが、emplace_back({...})はテンプレート引数推論に失敗しコンパイルエラーになる。- 速度: 差が大きく出るのは「既存オブジェクトをコピーする」ケース。ムーブが軽い型(
std::stringなど)の一時オブジェクトを渡す場合や、intのようなプリミティブ型では両者の差はほぼない。
「とりあえずemplace_back()にしておけば速い」ではなく、「emplace_back()は不要なコピー/ムーブの可能性を構造的に排除できる」という理解で使い分けるのが良いと思います。可読性の観点からは、単に既存オブジェクトを追加するだけならpush_back()の方が意図が明確な場合も多く、常にemplace_back()一択にする必要はありません。
付録: ベンチマークコード全文
実際に使用した計測コードの全体です。ビルドするには以下のコマンドを実行してください。
g++ -std=c++17 -O2 benchmark.cpp -o benchmarkclang++ -std=c++17 -O2 benchmark.cpp -o benchmark
reserve(N)で事前に容量を確保しているため、計測中に再配置(reallocation)は発生しません。
#include <chrono>
#include <iostream>
#include <string>
#include <vector>
// ある程度コストのかかるコンストラクタ・コピー・ムーブを持つクラス
struct Widget
{
int id;
std::string name; // ヒープ確保を伴う典型的なメンバ
Widget(int id_, std::string name_) : id(id_), name(std::move(name_)) {}
Widget(const Widget& other) : id(other.id), name(other.name) {}
Widget(Widget&& other) noexcept : id(other.id), name(std::move(other.name)) {}
};
constexpr int N = 2000000;
template <class F>
long long measure(F&& f)
{
auto start = std::chrono::high_resolution_clock::now();
f();
auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
}
int main()
{
// -------------------------------------------------
// 1. オブジェクトを渡す場合
// -------------------------------------------------
{
// 既存の左辺値をpush_back -> 毎回コピーコンストラクタが走る最悪ケース
std::vector<Widget> v;
v.reserve(N);
Widget w(0, "some_reasonably_long_name_for_sso_bypass");
auto ms = measure([&] {
for (int i = 0; i < N; ++i) {
v.push_back(w);
}
});
std::cout << "push_back(既存の左辺値w) : " << ms << " ms\n";
}
{
std::vector<Widget> v;
v.reserve(N);
auto ms = measure([&] {
for (int i = 0; i < N; ++i) {
v.push_back(Widget(i, "some_reasonably_long_name_for_sso_bypass"));
}
});
std::cout << "push_back(Widget(...)) : " << ms << " ms\n";
}
{
std::vector<Widget> v;
v.reserve(N);
auto ms = measure([&] {
for (int i = 0; i < N; ++i) {
v.emplace_back(i, "some_reasonably_long_name_for_sso_bypass");
}
});
std::cout << "emplace_back(i, name) : " << ms << " ms\n";
}
// -------------------------------------------------
// 2. 値(int)を渡す場合。std::vector<int>ではpush_backとemplace_backに差が出にくい例
// -------------------------------------------------
{
std::vector<int> v;
v.reserve(N);
auto ms = measure([&] {
for (int i = 0; i < N; ++i) {
v.push_back(i);
}
});
std::cout << "push_back(int) : " << ms << " ms\n";
}
{
std::vector<int> v;
v.reserve(N);
auto ms = measure([&] {
for (int i = 0; i < N; ++i) {
v.emplace_back(i);
}
});
std::cout << "emplace_back(int) : " << ms << " ms\n";
}
// -------------------------------------------------
// 3. {}(波括弧初期化)を渡す場合。push_backのみ計測(emplace_backはコンパイル不可のため対象外)
// -------------------------------------------------
{
std::vector<Widget> v;
v.reserve(N);
auto ms = measure([&] {
for (int i = 0; i < N; ++i) {
v.push_back({i, "some_reasonably_long_name_for_sso_bypass"});
}
});
std::cout << "push_back({i, name}) : " << ms << " ms\n";
}
{
// emplace_backで同等の処理をする場合は引数を展開して渡す
std::vector<Widget> v;
v.reserve(N);
auto ms = measure([&] {
for (int i = 0; i < N; ++i) {
v.emplace_back(i, "some_reasonably_long_name_for_sso_bypass");
}
});
std::cout << "emplace_back(i, name) [同等]: " << ms << " ms\n";
}
return 0;
}実行結果の一例(3回実施)
g++でコンパイルした結果
push_back(既存の左辺値w) : 101 ms push_back(Widget(...)) : 68 ms emplace_back(i, name) : 78 ms push_back(int) : 0 ms emplace_back(int) : 0 ms push_back({i, name}) : 63 ms emplace_back(i, name) [同等]: 78 ms push_back(既存の左辺値w) : 98 ms push_back(Widget(...)) : 64 ms emplace_back(i, name) : 79 ms push_back(int) : 0 ms emplace_back(int) : 0 ms push_back({i, name}) : 63 ms emplace_back(i, name) [同等]: 78 ms push_back(既存の左辺値w) : 101 ms push_back(Widget(...)) : 64 ms emplace_back(i, name) : 78 ms push_back(int) : 0 ms emplace_back(int) : 0 ms push_back({i, name}) : 64 ms emplace_back(i, name) [同等]: 77 msclang++でコンパイルした結果
push_back(既存の左辺値w) : 96 ms push_back(Widget(...)) : 63 ms emplace_back(i, name) : 65 ms push_back(int) : 0 ms emplace_back(int) : 0 ms push_back({i, name}) : 58 ms emplace_back(i, name) [同等]: 65 ms push_back(既存の左辺値w) : 112 ms push_back(Widget(...)) : 60 ms emplace_back(i, name) : 65 ms push_back(int) : 0 ms emplace_back(int) : 0 ms push_back({i, name}) : 58 ms emplace_back(i, name) [同等]: 65 ms push_back(既存の左辺値w) : 97 ms push_back(Widget(...)) : 60 ms emplace_back(i, name) : 65 ms push_back(int) : 0 ms emplace_back(int) : 0 ms push_back({i, name}) : 58 ms emplace_back(i, name) [同等]: 65 ms

ディスカッション
コメント一覧
まだ、コメントがありません