Effective Modern C++, Chapter 8

Tweaks

Two final items that don't fit neatly into the earlier chapters, but are critical for writing efficient modern C++: when pass-by-value is reasonable, and when emplacement beats insertion.

Prerequisites: Chapters 1-7. Move semantics, perfect forwarding, smart pointers.
9
Chapters
3+
Simulations

Chapter 0: The Problem

You're writing a function that always copies its parameter into a data structure. You want it to be efficient for both lvalues (copy) and rvalues (move). What's the best approach?

c++
class Widget {
public:
    // Approach 1: Overload for lvalues and rvalues
    void addName(const std::string& newName)
    { names.push_back(newName); }         // copy
    void addName(std::string&& newName)
    { names.push_back(std::move(newName)); }  // move

    // Approach 2: Universal reference template
    template<typename T>
    void addName(T&& newName)
    { names.push_back(std::forward<T>(newName)); }

    // Approach 3: Pass by value (!)
    void addName(std::string newName)
    { names.push_back(std::move(newName)); }
private:
    std::vector<std::string> names;
};
The trade-off: Overloading works but doubles your code. Universal references work but have template bloat, scary error messages, and can't handle all types (Item 30). Pass by value is one function that handles both — at the cost of one extra move.
Why does approach 3 call std::move(newName) inside the function body?

Chapter 1: Consider Pass by Value

The title of Item 41 is very precise: "Consider pass by value for copyable parameters that are cheap to move and always copied." Every word matters.

c++
Widget w;
std::string name("Bart");

w.addName(name);             // lvalue: copy-construct param + move into vector
w.addName(name + "Jenne");    // rvalue: move-construct param + move into vector
C++11 made pass-by-value viable again. In C++98, pass by value always meant a copy. In C++11, rvalue arguments are move-constructed into the parameter. Lvalues are still copied, but then the parameter is moved into its final destination.

Four conditions for pass by value

1. You should only CONSIDER it
It's not always optimal. It has one extra move compared to by-reference approaches. Profile when in doubt.
2. Copyable parameters only
For move-only types (e.g., unique_ptr), you only need one overload (rvalue ref). No benefit from pass-by-value.
3. Cheap to move
If moves are expensive (e.g., std::array), the extra move is costly. Only use when moves are O(1).
4. Always copied
If the function might not copy (e.g., validation check first), you pay for construction even when nothing is stored.
Why is pass by value not useful for move-only types like std::unique_ptr?

Chapter 2: Cost Analysis

Let's count copy and move operations for each approach when adding a name to a Widget.

Operation Cost Comparison

Click to compare costs for lvalue vs rvalue arguments.

ApproachLvalue CostRvalue Cost
Overloading1 copy1 move
Universal ref1 copy1 move
Pass by value1 copy + 1 move2 moves
The extra move is the price of simplicity. For cheap-to-move types like std::string and std::vector, one extra move is typically negligible. But it adds up in function call chains (see next chapter).
Compared to the overloading approach, pass by value incurs how much extra cost?

Chapter 3: Caveats

Pass by value has hidden costs that the simple "one extra move" analysis misses.

Construction vs assignment

c++
class Password {
    std::string text;
public:
    void changeTo(std::string newPwd)      // pass by value
    { text = std::move(newPwd); }         // assignment, not construction
};

std::string initPwd("Supercalifragilisticexpialidocious");
Password p(initPwd);

std::string newPwd("Beware the Jabberwock");
p.changeTo(newPwd);  // copies newPwd, then move-assigns to text
// But text already had enough capacity! With by-ref, no allocation needed.
The problem: Pass by value forces a copy-construction of the parameter (allocation), then move-assigns to the member (deallocation of old buffer). With the overloading approach (const string&), assignment can reuse the existing buffer if it's big enough. Two memory operations vs zero.

Function call chains

c++
// Each function takes by value: "only one extra move"
void addName(std::string name) {
    validateName(name);  // another by-value copy!
    ...
}
void validateName(std::string name) {
    logName(name);       // yet another!
    ...
}
// "One extra move" per function * N functions = N extra moves
// By-reference chains incur zero accumulated overhead

Slicing

c++
class Widget { ... };
class SpecialWidget : public Widget { ... };

void process(Widget w);  // pass by value: SLICES derived types!
SpecialWidget sw;
process(sw);            // sw's SpecialWidget-ness is lost

The complete picture: when to use what

ScenarioBest ApproachWhy
Copyable, cheap-to-move, always copied, no slicingPass by valueSimple, one function, minimal overhead
Move-only types (unique_ptr, future)Rvalue ref overloadOnly one overload needed anyway
Assignment-based copying (setters)Overloading or universal refCan reuse existing buffer capacity
Function call chainsBy referenceNo accumulated move overhead
Base class parametersBy referenceAvoids slicing
Maximum performance neededOverloading or universal refZero extra overhead
Bottom line: Pass by value is appropriate for copyable, cheap-to-move types that are always copied, not in call chains, and not base class types. For everything else, use overloading or universal references.
Why can assignment-based copying be much more expensive with pass by value?

Chapter 4: Emplacement vs Insertion

When you add a string literal to a vector<string> via push_back, a temporary std::string is created. emplace_back can avoid that entirely.

c++
std::vector<std::string> vs;

vs.push_back("xyzzy");   // creates temp string, then moves it in
vs.emplace_back("xyzzy"); // constructs string DIRECTLY in the vector

What push_back actually does

Step 1
Create a temporary std::string from "xyzzy" (construction #1)
Step 2
Move-construct a new element in the vector from the temporary (construction #2)
Step 3
Destroy the temporary (destructor call)

What emplace_back does

Step 1
Construct a std::string directly in the vector from "xyzzy" (one construction, zero temporaries)
How it works: Emplacement functions take constructor arguments, not objects. They use perfect forwarding to construct the element in-place. This avoids creating and destroying temporaries.

Every container has emplacement counterparts: emplace_back, emplace_front, emplace, emplace_hint, emplace_after.

c++
// emplace_back forwards any arguments to the string constructor
vs.emplace_back(50, 'x');  // constructs string(50, 'x') directly in-place
What is the key difference between push_back and emplace_back?

Chapter 5: When to Emplace

In theory, emplacement is never slower than insertion. In practice, it depends. Here's a heuristic for when emplacement is most likely to win:

ConditionWhy It Matters
Value is constructed into container, not assignedAssignment into occupied slots may create a temporary anyway (the element to move from)
Argument types differ from container typeIf you're already passing a std::string to a vector<string>, no temporary is needed for insertion either
Container won't reject as duplicateEmplacement creates a node to compare, then destroys it if rejected — wasted construction
All three conditions = emplacement wins. If any condition fails, benchmark to be sure. Node-based containers (list, map, set) almost always use construction. emplace_back and emplace_front always construct.

Container types and construction vs assignment

Container TypeNode-based?Uses Construction?
std::list, std::forward_listYesAlways (new nodes)
std::map, std::setYesAlways (new nodes)
std::unordered_map, std::unordered_setYesAlways (new nodes)
std::vectorNoemplace_back: yes. emplace at middle: may assign.
std::dequeNoemplace_back/front: yes. Middle: may assign.
std::stringNoDepends on position

Danger: explicit constructors

c++
std::vector<std::regex> regexes;

regexes.push_back(nullptr);    // ERROR: won't compile (implicit conversion rejected)
regexes.emplace_back(nullptr); // COMPILES! Direct init uses explicit ctor. UB at runtime.

// Because:
std::regex r1 = nullptr;  // copy init: explicit ctor blocked → error
std::regex r2(nullptr);   // direct init: explicit ctor allowed → compiles (UB)
Insertion uses copy initialization (can't use explicit ctors). Emplacement uses direct initialization (can use explicit ctors). This means emplacement can silently compile code that insertion would reject. Be extra careful with emplacement arguments.

Copy init vs direct init: the rule

SyntaxInitialization TypeExplicit Ctors?Example
T x = expr;Copy initializationBlockedstd::regex r = nullptr; (error)
T x(expr);Direct initializationAllowedstd::regex r(nullptr); (compiles, UB)
push_back(expr)Copy initializationBlockedSafe: rejects bad conversions
emplace_back(expr)Direct initializationAllowedRisky: accepts bad conversions

Danger: exception safety with new

c++
std::list<std::shared_ptr<Widget>> ptrs;

// push_back: safe. Temporary shared_ptr created BEFORE push_back.
// If allocation fails, shared_ptr destructor cleans up.
ptrs.push_back(std::shared_ptr<Widget>(new Widget, killWidget));

// emplace_back: UNSAFE. Raw pointer forwarded into container.
// If node allocation throws, raw pointer is leaked!
ptrs.emplace_back(new Widget, killWidget);

// Fix: create the shared_ptr first, then emplace the rvalue
auto spw = std::shared_ptr<Widget>(new Widget, killWidget);
ptrs.emplace_back(std::move(spw));
Why does regexes.emplace_back(nullptr) compile while regexes.push_back(nullptr) doesn't?

Chapter 6: Cost Visualized

See how the three parameter-passing approaches compare, and how emplacement vs insertion differs.

Push vs Emplace: String Literal

Adding "xyzzy" to a vector<string>. Click to compare.

Parameter Passing Cost

Three approaches to addName, compared for lvalue and rvalue.

The emplace_back decision flowchart

Is the value being CONSTRUCTED into the container?
If it's being assigned (e.g., emplace at an occupied position), emplacement may still need a temporary.
↓ yes
Do the argument types DIFFER from the container element type?
If you're passing a std::string to a vector<string>, insertion needs no temporary anyway.
↓ yes
Will the container ACCEPT the value (no duplicate rejection)?
If rejected (sets/maps), the node is constructed then destroyed for nothing.
↓ yes
Use emplace_back!
Emplacement will almost certainly outperform insertion in this case.

When emplacement does NOT help

c++
std::vector<std::string> vs;
std::string queen("Donna Summer");

// Same type passed — no temporary either way
vs.push_back(queen);     // copy-construct into vector
vs.emplace_back(queen);  // also copy-construct. No difference.

// Emplacement at occupied position — may need temporary for move-assign
vs.emplace(vs.begin(), "xyzzy");  // move-assigns, may create temp
When passing a string literal to push_back, how many constructor calls occur?

Chapter 7: Beyond

ItemKey Takeaway
Item 41Consider pass by value for copyable, cheap-to-move params that are always copied. One extra move vs by-ref. Don't use for assignment-based copying, call chains, or base classes.
Item 42Emplacement constructs in-place (constructor args, not objects). Fastest when constructing into container with non-matching types and no duplicates. Watch out for explicit ctors and exception safety.
That's all 42 items. You've covered type deduction, auto, modern idioms, smart pointers, move semantics, lambdas, concurrency, and performance tweaks. Go build something.

Emplacement function reference

ContainerInsertionEmplacement
vector, deque, stringpush_back, push_front*, insertemplace_back, emplace_front*, emplace
listpush_back, push_front, insertemplace_back, emplace_front, emplace
forward_listpush_front, insert_afteremplace_front, emplace_after
set, map, multiset, multimapinsertemplace, emplace_hint
unordered_set, unordered_mapinsertemplace, emplace_hint

* push_front/emplace_front not available for vector and string.

Key principles from Effective Modern C++

Understand, don't memorize
The rationale behind each Item matters more than the advice itself. Understand why and you'll know when to break the rules.
Modern C++ is about ownership
Smart pointers encode ownership in types. Move semantics transfer it efficiently. Lambdas capture it safely. Every new feature serves this theme.
Efficiency through expressiveness
auto, lambdas, and emplacement aren't just convenient — they often generate better code. Expressiveness and performance are aligned in modern C++.

The Complete Item Index

ChItemsTopic
11-4Deducing Types
25-6auto
37-17Moving to Modern C++
418-22Smart Pointers
523-30Rvalue Refs, Move, Forwarding
631-34Lambda Expressions
735-40The Concurrency API
841-42Tweaks

"The most important part of each Item is not the advice it offers, but the rationale behind the advice." — Scott Meyers