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

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.