Don't blindly prefer `emplace_back` to `push_back` (2021)

C++ developers debate when to use `emplace_back` instead of `push_back` in standard containers, noting that while `emplace_back` can construct objects in place and avoid copies, it also introduces more complex semantics and potential for subtle bugs. Several argue that `push_back` is clearer and usually sufficient, especially when constructor behavior or implicit conversions aren’t obvious to readers or reviewers, and that tools which blindly suggest replacing it can degrade code quality or performance. The exchange broadens into reflections on C++’s growing complexity, the limits of compiler optimizations, and how other languages like Rust, Go, or C# handle object construction and ownership more transparently.

Semantics of push_back vs emplace_back

  • push_back takes an already-constructed object and copies/moves it into the container.
  • emplace_back forwards constructor arguments and constructs the element directly in the container’s storage.
  • Example given: with a type that logs constructors, emplace_back(args…) calls only the relevant ctor, while push_back(T(args…)) calls the value ctor plus a move/copy ctor, and may duplicate work inside subobjects (e.g., std::string data).

Correctness, readability, and intent

  • Several commenters prefer push_back(T(...)) for clarity: it’s explicit about which type is being constructed.
  • emplace_back(args…) can be ambiguous without knowing the container’s element type and its constructors.
  • A key pitfall: for std::vector<std::vector<int>>, emplace_back(1<<20) constructs a huge inner vector instead of appending an int; push_back(1<<20) would fail to compile, which is safer.
  • Suggested rule of thumb:
    • Use push_back when you already have an object or want aggregate/designated initialization.
    • Use emplace_back when constructing directly in-place, especially for non-copyable or expensive-to-copy types.

Tooling and compiler behavior

  • Older clang-tidy checks (“modernize-use-emplace”) encouraged replacing push_back with emplace_back, sometimes inappropriately; newer versions can now warn about unnecessary temporaries even with emplace_back.
  • Compilers can often elide temporaries, but not when copies/moves have observable side effects; emplace_back expresses intent rather than relying on optimization.
  • emplace_back is a template, so may marginally increase compile times compared to push_back.

Performance and “real-world” impact

  • Some argue the micro-performance difference is negligible in many domains (e.g., GUI construction) and that time spent on such minutiae is overblown.
  • Others respond that understanding and using the right tool improves code quality and maintains invariants (e.g., for non-copyable types), even when speed isn’t critical.

Broader language and ecosystem commentary

  • Discussion branches into C++ complexity (rvalue refs, value categories) and whether this mental overhead is justified.
  • Comparisons are made with Rust, Go, and C#:
    • Rust also wrestled with placement APIs and uses MaybeUninit patterns instead.
    • Go/C# are seen as simpler, but less powerful in some scenarios.