Effective Modern C++

Effective Modern C++ · Scott Meyers ·334 pages

42 guidelines for effective C++11/14: type deduction (templates/auto/decltype), modern initialization, nullptr/constexpr/override/noexcept, smart pointers, rvalue references, move semantics, perfect forwarding, lambdas, and concurrency with futures and thread_local.

Capabilities (6)
  • Apply all 42 guidelines for C++11/14 correctness and performance
  • Explain type deduction rules for templates, auto, and decltype
  • Choose between unique_ptr, shared_ptr, and weak_ptr for ownership semantics
  • Apply move semantics and perfect forwarding correctly — avoid the common traps
  • Write efficient lambdas with correct capture modes and avoid dangling references
  • Use std::async and std::future vs. std::thread with correct thread management
How to use

Install this skill and Claude can apply all 42 Effective Modern C++ guidelines to audit C++11/14 code, trace type deduction through templates and auto, diagnose move-semantics failures, and rewrite lambda captures to be safe and correct

Why it matters

C++11/14 introduced enough new features that code can look modern but harbor subtle correctness bugs — moves that silently fall back to copies, captures that dangle, threads that terminate on destruction — and this skill catches all of them

Example use cases
  • Tracing why auto x = {1, 2, 3} deduces initializer_list<int> while auto x(1) deduces int and recommending explicit types where the behavior is surprising
  • Identifying a class whose vector reallocation copies instead of moves because its move constructor is missing the noexcept annotation
  • Spotting a lambda that captures this via [=] in an async callback that outlives the object and rewriting it using [self = shared_from_this()]

Effective Modern C++

A complete reference for the 42 guidelines Scott Meyers distilled from C++11 and C++14, covering when and why to apply every major modern C++ feature — not just what the features are.

Source type: technical | Domain: C++ programming | Audience: C++ developers migrating to C++11/14/17


Core Philosophy

The book’s central thesis: C++11/14 is not “C++98 with additions” — it is effectively a new language. Meyers’ guidelines exist not to describe features but to govern their effective application: software that is correct, efficient, maintainable, and portable.

Key operating principles:

  • Guidelines have exceptions. The rationale behind each Item matters more than the rule itself. Understand the why before deciding whether your situation justifies deviation.
  • Prefer explicitness over implicit cleverness. override, noexcept, = delete, = default, nullptr, scoped enums — each makes intent visible and catches bugs at compile time.
  • Move semantics are not magic. They reduce copies only when the right conditions hold. Assuming otherwise leads to subtle performance regressions.
  • Type deduction is powerful but opaque. Auto and templates save typing, but you must understand what the compiler deduces, or surprises follow.
  • The C++ concurrency API is a high-level abstraction. Use it; do not fight it by managing threads manually unless you have a specific reason.

Key Guidelines (Items)

Chapter 1: Deducing Types

Item 1: Understand template type deduction.

Three cases based on the form of ParamType:

  1. Reference or pointer (not universal ref): Strip the reference from the argument’s type, then pattern-match against ParamType. const is preserved. void f(T& p); f(cx) where cx is const int → T = const int, param = const int&.
  2. Universal reference (T&&): If the argument is an lvalue, both T and ParamType become lvalue references (the only case where T is deduced as a reference). If an rvalue, normal rules apply.
  3. Pass by value: Strip reference-ness, then strip const/volatile. A copy can be modified; the original’s constness is irrelevant to the copy.

Edge cases: Array and function arguments decay to pointers for by-value params, but if the param is a reference, T is deduced as the actual array type (e.g., const char[13]), enabling compile-time arraySize templates.

Item 2: Understand auto type deduction.

Auto type deduction is template type deduction with one exception: a braced initializer ({...}) deduces to std::initializer_list<T> with auto, but fails to compile with a raw template parameter. Consequence: auto x = {27} gives std::initializer_list<int>, not int. In C++14, auto return types and lambda auto parameters use template deduction rules, not auto deduction rules — so returning {1,2,3} from an auto-return function fails.

Item 3: Understand decltype.

decltype parrots back the declared type of a name or expression without modification. Primary use: trailing return types where the return type depends on parameters. decltype(auto) (C++14) tells the compiler to deduce a type but apply decltype rules — preserving references that plain auto would strip. Critical trap: decltype((x)) is int& even though decltype(x) is int — parentheses around a name create an lvalue expression, causing decltype to report a reference type. Returning (x) from a decltype(auto) function returns a dangling reference to a local.

Item 4: Know how to view deduced types.

Three tools in order of reliability: (1) IDE hover — convenient but sometimes wrong for complex types. (2) Compiler error — declare template<typename T> class TD; (undefined) and attempt TD<decltype(x)> xType;; the error message contains the deduced type. (3) Runtime — typeid(x).name() strips cv-qualifiers and references per the standard, so it lies. Use boost::typeindex::type_id_with_cvr<T>().pretty_name() for accurate runtime type names.


Chapter 2: auto

Item 5: Prefer auto to explicit type declarations.

Benefits: Forces initialization (no uninitialized variables). Correctly names closure types (which can’t be written out). Avoids type mismatches like using int for std::vector<int>::size_type on 64-bit platforms. Avoids the subtle std::pair<const std::string, int> vs std::pair<std::string, int> mismatch when iterating maps.

Item 6: Use the explicitly typed initializer idiom when auto deduces undesired types.

std::vector<bool>::operator[] returns a proxy object (std::vector<bool>::reference), not bool&. auto highPriority = features[5]; deduces a dangling proxy. Fix: auto highPriority = static_cast<bool>(features[5]);. The broader principle: when auto deduces an “invisible” proxy type, use a cast to force the intended type rather than abandoning auto entirely.


Chapter 3: Moving to Modern C++

Item 7: Distinguish between () and {} when creating objects.

Braced initialization advantages: works everywhere (in-class member initializers, uncopyable types like std::atomic), prohibits narrowing conversions, immune to the “most vexing parse” (Widget w2() declares a function; Widget w3{} calls default ctor).

The gotcha: when a constructor taking std::initializer_list exists, brace initialization strongly prefers it — even over better-matching non-initializer-list constructors, and even if it requires narrowing-prohibited conversions (which then produce an error). std::vector<int> v1(10, 20) creates 10 elements of value 20; std::vector<int> v2{10, 20} creates 2 elements. Pick a style (braces-default or parens-default) and be consistent. As a class author, design constructors so the choice of delimiter doesn’t produce different behavior.

Item 8: Prefer nullptr to 0 and NULL.

0 and NULL are integral types, not pointer types. They create ambiguity in overload resolution when both pointer and integer overloads exist. nullptr’s type (std::nullptr_t) implicitly converts to all raw pointer types but not to integer types. In template code, 0 and NULL deduce as int, breaking pointer-expecting templates. Always use nullptr.

Item 9: Prefer alias declarations to typedefs.

using alias declarations are cleaner (using UPtrMapSS = std::unique_ptr<std::unordered_map<std::string, std::string>>), and unlike typedef, alias templates are directly parameterizable: template<typename T> using MyAllocList = std::list<T, MyAlloc<T>>;. With typedef, alias templates require an extra template<> struct wrapper and typename qualifications. In C++14, the standard library uses alias templates for all _t traits (std::remove_reference_t<T> instead of typename std::remove_reference<T>::type).

Item 10: Prefer scoped enums to unscoped enums.

Unscoped (enum Color) enumerators leak into the enclosing scope and implicitly convert to integers. Scoped (enum class Color) enumerators are namespace-contained (Color::red), have no implicit conversion to integers, and can be forward-declared with a fixed underlying type. Exception: when you deliberately want implicit integer conversion (e.g., using enum values as indices into tuples or arrays), unscoped enums are acceptable.

Item 11: Prefer deleted functions to private undefined ones.

= delete works for any function (including non-member functions and template specializations), generates clear compiler errors at the call site rather than confusing linker errors, and can block specific instantiations: template<> void processPointer<void>(void*) = delete;.

Item 12: Declare overriding functions override.

Virtual function overriding requires: same name, same parameter types, same constness, same reference qualifiers (new in C++11), and compatible return types. Missing any condition silently creates a new non-overriding virtual instead of overriding. override keyword makes the compiler verify all conditions. Use it on every intended override. Reference qualifiers on member functions (e.g., void doWork() &; for lvalues only, void doWork() &&; for rvalues) allow different behaviors based on whether *this is an lvalue or rvalue.

Item 13: Prefer const_iterators to iterators.

In C++11, cbegin() and cend() are member functions on all containers. In generic code, use non-member std::cbegin(container) and std::cend(container) (C++14). Prefer const_iterator whenever you don’t need to modify elements; it documents intent and can be more efficient.

Item 14: Declare functions noexcept if they won’t emit exceptions.

noexcept is a stronger contract than throw() (C++98): compilers can optimize assuming no exceptions will unwind the stack. Critical for move operations: STL containers (e.g., std::vector::push_back) use move instead of copy during reallocation only if the move constructor is noexcept (to maintain strong exception safety). If your move constructor isn’t noexcept, containers will copy instead. Functions that are genuinely noexcept: memory deallocation functions, destructors (implicitly noexcept unless specified otherwise), and leaf functions with no throwing calls.

Item 15: Use constexpr whenever possible.

constexpr objects are const and have values known at compile time. constexpr functions may be evaluated at compile time (guaranteed if all arguments are compile-time constants) or at runtime. In C++11, constexpr functions are limited to a single return statement. C++14 lifts this: loops, conditionals, and multiple statements are allowed. Practical uses: constexpr array sizes derived from other arrays, lookup tables computed at compile time, static_assert conditions, template non-type arguments.

Item 16: Make const member functions thread safe.

A const member function is expected to be safe to call concurrently. If it modifies mutable members (e.g., a cached result), use std::mutex or std::atomic to protect the mutation. For a single variable (a cache value), std::atomic is sufficient and cheaper. For multiple variables that must be mutated atomically together, use std::mutex. Copying a std::mutex or std::atomic is deleted, so classes containing them are not copyable — provide explicit copy/move operations if needed or suppress them.

Item 17: Understand special member function generation.

C++11 special member functions: default ctor, destructor, copy ctor, copy assignment, move ctor, move assignment.

Move operations are generated only when: (1) no copy operations are declared, (2) no move operations are declared, (3) no destructor is declared. The Rule of Three becomes the Rule of Five in C++11: if you declare any of {destructor, copy ctor, copy assignment, move ctor, move assignment}, consider declaring all five.

Declaring a move operation suppresses generation of copy operations (deleted). Declaring a copy operation suppresses generation of move operations. Declaring a destructor suppresses move generation and (deprecated, but still compiles) affects copy.

Use = default to explicitly request compiler-generated implementations. This is especially important for polymorphic base classes: declare a virtual destructor, then use = default for move and copy to preserve movability and copyability.


Chapter 4: Smart Pointers

Item 18: Use std::unique_ptr for exclusive-ownership resource management.

std::unique_ptr is move-only, the same size as a raw pointer by default, and the appropriate default smart pointer. Use as factory return type — callers receive efficient exclusive ownership, and can convert to shared_ptr if shared ownership is later needed. Custom deleters: specify as the second template type argument. Stateless deleters (captureless lambdas) add no size penalty; function pointers double the size. Use lambdas over function pointers as deleters. For factory functions returning class hierarchies, the base class must have a virtual destructor when the deleter receives a base class pointer.

Item 19: Use std::shared_ptr for shared-ownership resource management.

std::shared_ptr is twice the size of a raw pointer (raw pointer to resource + raw pointer to control block). The control block contains: reference count (atomic), weak count (atomic), custom deleter (if any), allocator. Performance implications: atomic reference count operations are slower than non-atomic. Moving a shared_ptr is faster than copying — no reference count change needed. Custom deleters don’t affect the size of the shared_ptr object itself (unlike unique_ptr), because the deleter lives in the control block on the heap.

Control block creation rules: make_shared always creates one. Constructing from a raw pointer creates one. Constructing from unique_ptr or weak_ptr creates one from the existing control block. Never create two shared_ptrs from the same raw pointer — two control blocks, double destruction, undefined behavior. If a class needs shared_ptr to itself, inherit from std::enable_shared_from_this<T> and call shared_from_this() rather than shared_ptr<T>(this).

Item 20: Use std::weak_ptr for std::shared_ptr-like pointers that can dangle.

std::weak_ptr points to the same object as a shared_ptr without affecting the reference count. Check expiration with wpw.expired(). To dereference, convert atomically (check + lock in one operation): auto spw = wpw.lock() (returns null shared_ptr if expired) or std::shared_ptr<Widget> spw(wpw) (throws std::bad_weak_ptr if expired). Never check expired() then dereference separately — race condition.

Use cases: (1) Caching: cache holds weak_ptr; factory function promotes to shared_ptr if not expired, reloads otherwise. (2) Observer lists: subjects hold weak_ptr to observers, checking expiry before notification. (3) Breaking shared_ptr cycles: in parent-child graphs where children need pointers back to parents, use weak_ptr for the back-pointer to avoid reference cycles that prevent destruction.

Item 21: Prefer std::make_unique and std::make_shared to direct use of new.

Three reasons: (1) Avoids type repetition (auto p = std::make_shared<Widget>() vs std::shared_ptr<Widget> p(new Widget)). (2) Exception safety: processWidget(std::shared_ptr<Widget>(new Widget), computePriority()) can leak if computePriority() throws between new Widget and shared_ptr construction. make_shared is atomic — no leak window. (3) make_shared allocates the object and control block in a single allocation (faster, better cache locality) vs. two allocations with direct new.

When make functions can’t be used: custom deleters, brace-initialized objects (make functions use (), not {}), very large objects where you need to release memory before all weak_ptrs expire (with make_shared, the control block and object share one allocation, delaying memory release).

Item 22: When using the Pimpl Idiom, define special member functions in the implementation file.

std::unique_ptr<Impl> as a data member works for Pimpl only if the special member functions (destructor, move operations) are defined in the .cpp file where Impl is complete. The compiler-generated destructor for the outer class calls unique_ptr’s destructor, which calls delete on the raw pointer — but delete on an incomplete type is undefined behavior. Solution: declare the destructor in the header, define it as = default in the .cpp file after the Impl definition. shared_ptr does not have this problem (deleter type is not part of the pointer type, erased at construction).


Chapter 5: Rvalue References, Move Semantics, and Perfect Forwarding

Item 23: Understand std::move and std::forward.

std::move performs an unconditional cast to rvalue. It does not move anything at runtime — it generates no code. It is merely static_cast<T&&>(param) with remove_reference applied. Do not declare objects const if you want to move from them: std::move on a const T produces const T&&, which binds to the copy constructor (which takes const T&), not the move constructor (which takes T&&). The object is silently copied.

std::forward performs a conditional cast to rvalue — only if its argument was initialized with an rvalue. It encodes lvalue/rvalue information in the template parameter T (via reference collapsing). Use std::move on rvalue references; use std::forward on universal references. Never the other way around.

Item 24: Distinguish universal references from rvalue references.

T&& means universal reference only when type deduction occurs on T. Without type deduction, T&& is an rvalue reference. The exact form must be T&&const T&& or std::vector<T>&& are rvalue references even inside a template. Universal references arise in: (1) function template parameters template<typename T> void f(T&& param), (2) auto&& declarations. push_back(T&&) in std::vector<T> is an rvalue reference (T already deduced for the instantiation); emplace_back(Args&&... args) uses universal references (Args deduced fresh on each call).

Item 25: Use std::move on rvalue references, std::forward on universal references.

Rvalue reference parameters are always bound to rvalues (the caller explicitly moved). Cast them unconditionally with std::move. Universal reference parameters may be bound to lvalues or rvalues; cast conditionally with std::forward<T>. Misusing std::move on a universal reference corrupts lvalue arguments — passing a local variable to setName(T&& newName) { name = std::move(newName); } silently empties the local. If the parameter is the last use, std::move/std::forward on return: return std::move(rhs). However, do not apply std::move or std::forward to local variables being returned by value — compilers apply NRVO (Named Return Value Optimization) which is better; forcing a move can prevent NRVO.

Item 26: Avoid overloading on universal references.

A function taking a universal reference is an extremely greedy overload — it will match anything that doesn’t require a conversion. It beats const Widget& overloads for non-const lvalues, beats copy constructors for non-const objects, and causes surprising resolution for near-matches. The problem compounds with perfect-forwarding constructors: they can hijack copy construction when the argument is a non-const lvalue of the class type.

Item 27: Familiarize yourself with alternatives to overloading on universal references.

Alternatives to universal reference overloads: (1) Pass by value — acceptable when parameter is always copied (see Item 41). (2) Pass by const T& — forfeits some move efficiency, often acceptable. (3) Tag dispatch — add an extra std::true_type/std::false_type parameter to disambiguate overloads. (4) std::enable_if — constrain the universal reference overload to only apply to non-Widget types: template<typename T, typename = std::enable_if_t<!std::is_base_of<Widget, std::decay_t<T>>::value>> Widget(T&& rhs). std::decay_t<T> strips references and cv-qualifiers for the check.

Item 28: Understand reference collapsing.

Reference collapsing applies when a reference to a reference would form. Rule: if either reference is an lvalue reference, the result is an lvalue reference. If both are rvalue references, the result is an rvalue reference. Reference collapsing occurs in: template instantiation, auto deduction, typedef/using aliases, decltype. This mechanism is what makes universal references work — T&& with T = Widget& collapses to Widget& && = Widget&; with T = Widget it stays Widget&&.

Item 29: Assume that move operations are not present, not cheap, and not used.

When writing generic code, do not assume move semantics apply. C++98 types have no move operations (copies are used). Even move-enabled types may not be cheap: std::array move is O(n) (inline storage), std::string move may be no faster than copy for small strings (SSO stores content inline). STL containers use move during reallocation only if the move constructor is noexcept — otherwise they copy for strong exception safety. In code where you know the specific types involved, you can rely on their documented move behavior.

Item 30: Familiarize yourself with perfect forwarding failure cases.

Perfect forwarding fails when fwd(arg) and f(arg) behave differently. Failure cases:

  1. Braced initializers: fwd({1,2,3}) fails; braced initializers have no type for deduction. Fix: auto il = {1,2,3}; fwd(il);.
  2. 0 and NULL as null pointers: deduce as integer types. Use nullptr.
  3. Declaration-only integral static const data members: static const std::size_t MinVals = 28; without a definition — compilers can use the value inline, but taking a reference (which forwarding does) requires a definition. ODR-use requires a definition.
  4. Overloaded function names and function templates: passing an overloaded function name is ambiguous; the forwarding template can’t deduce which overload. Cast to the exact function pointer type first.
  5. Bitfields: no pointer/reference can bind to a bitfield. Copy it to a regular variable first.

Chapter 6: Lambda Expressions

Item 31: Avoid default capture modes.

[&] (default by-reference): closures hold references to local variables. If the closure outlives the locals (stored in a container, passed to async code), the references dangle. Explicit captures ([&x]) at least document the dependency and jog lifetime analysis.

[=] (default by-value): does not capture data members — it captures this. Inside Widget::addFilter, [=]{ return divisor; } silently captures this->divisor via the this pointer, not a copy of divisor. If the Widget is destroyed before the closure executes, dangling pointer. Fix: auto divisorCopy = divisor; [divisorCopy]{ return divisorCopy; }. In C++14, use init capture: [divisor = divisor]{ return divisor; }.

Additionally, [=] implies self-containment but static local variables are referenced (not captured), creating silent behavioral dependencies on external mutation.

Item 32: Use init capture to move objects into closures.

C++11 has no move capture. C++14 generalized lambda capture (init capture) solves this: [pw = std::move(pw)]{ return pw->isValidated(); }. Left side of = names the closure data member; right side is the initializer expression evaluated in the enclosing scope. For C++11, emulate with std::bind: bind a move-constructed argument to a lambda using std::bind(std::move(obj), ...).

Item 33: Use decltype on auto&& parameters to std::forward them.

In C++14 generic lambdas, parameters may be auto&& (universal references). To forward them, use std::forward<decltype(param)>(param). decltype on an lvalue universal reference gives T&; on an rvalue universal reference gives T&&. Reference collapsing makes this work correctly in both cases.

Item 34: Prefer lambdas to std::bind.

Lambdas are more readable — the code at the call site is visible. std::bind stores all arguments by value by default; passing by reference requires std::ref. std::bind is unclear about call-by-value vs. call-by-reference semantics and whether a stored argument is called immediately or at invocation time. In C++14, with generic lambdas and init capture, there is essentially nothing std::bind can express that a lambda cannot. In C++11, std::bind has limited use for move capture emulation and some multi-call scenarios, but otherwise prefer lambdas.


Chapter 7: The Concurrency API

Item 35: Prefer task-based programming to thread-based.

std::async(f) (task-based) vs. std::thread t(f) (thread-based). Task-based advantages: (1) Return values and exceptions are accessible via the returned std::future. Thread-based: no direct return value access; uncaught exceptions terminate the program. (2) The runtime handles thread creation, destruction, oversubscription, and load balancing. Thread-based: if too many threads are created, std::system_error is thrown; context-switching overhead from oversubscription is your problem. Use threads directly only when: you need platform-specific thread API access (native_handle), you need to optimize thread count for a known deployment, or you’re building concurrency primitives beyond the C++ API.

Item 36: Specify std::launch::async if asynchronicity is essential.

std::async’s default launch policy is std::launch::async | std::launch::deferred. The scheduler may run the function synchronously on the calling thread when get()/wait() is called. This means: (1) You can’t know if the function runs concurrently. (2) Thread-local variables accessed by the task may belong to the calling thread. (3) Timeout-based loops (wait_for(100ms)) can infinite-loop if the task is deferred — wait_for returns deferred status indefinitely. Fix: check fut.wait_for(0s) == std::future_status::deferred before entering such loops. If you need guaranteed asynchronous execution, use std::async(std::launch::async, f).

Item 37: Make std::threads unjoinable on all paths.

A joinable std::thread whose destructor runs terminates the program (std::terminate). Ensuring unjoinability means ensuring join() or detach() is called on every path, including exceptions. Use an RAII wrapper (the Standard Library has none — write your own ThreadRAII): capture the thread and a DtorAction (join or detach), call the action in the destructor. Implicit join in the destructor is usually wrong (silently waits when conditions aren’t met); implicit detach is even worse (detached thread accesses stack memory after its frame is popped, causing silent corruption).

Item 38: Be aware of varying thread handle destructor behavior.

Unlike std::thread (terminates on joinable destruction), a future’s destructor behavior depends on shared state:

  • Normal behavior: destroys the future object, decrements ref count in shared state.
  • Exception: the last future referring to a shared state created by std::async with std::launch::async policy blocks until the task completes (implicit join).

This means a std::vector<std::future<void>> or a class containing a std::shared_future might block in its destructor. std::packaged_task-created futures follow normal behavior.

Item 39: Consider void futures for one-shot event communication.

For detecting task → reacting task communication: condition variables require a mutex (feels wrong if there’s no shared data), can miss notifications if detecting fires before reacting waits, and are susceptible to spurious wakeups. Shared atomic flag avoids those problems but polls (wastes CPU, no true blocking). Combined condvar + flag works but is stilted. Alternative: std::promise<void> + std::future<void>. Detecting task calls p.set_value(); reacting task calls p.get_future().wait(). No mutex needed, no missed notification, no spurious wakeup, true blocking. Limitation: one-shot only. For multiple reacting tasks, use p.get_future().share() to get a std::shared_future<void> captured by value in each lambda.

Item 40: Use std::atomic for concurrency, volatile for special memory.

std::atomic<T> guarantees: (1) all operations are atomic (no partial reads/writes from other threads), (2) restrictions on compiler/hardware code reordering around atomic accesses (sequential consistency by default). RMW operations like ++ai are atomic.

volatile guarantees: reads and writes will not be optimized away (redundant reads and dead stores preserved). It says nothing about atomicity or reordering — two threads incrementing a volatile int produces a data race with undefined behavior.

Use std::atomic for inter-thread shared flags and counters. Use volatile for memory-mapped I/O registers where reads and writes must not be elided. They can be combined (volatile std::atomic<int>) for memory-mapped locations accessed from multiple threads. std::atomic copy operations are deleted (reading + writing in a single atomic op is generally unsupported by hardware); use .load() and .store() for explicit copies.


Chapter 8: Tweaks

Item 41: Consider pass by value for copyable parameters that are cheap to move and always copied.

The pattern: when a function always copies its parameter into storage (e.g., names.push_back(newName)), consider taking the parameter by value: void addName(std::string newName) { names.push_back(std::move(newName)); }. Cost analysis: one copy for lvalues (same as const ref overload), one move for rvalues (same as rvalue ref overload), but only one function to maintain. The cost is one extra move over the two-overload approach. Only appropriate when: (1) the parameter type is copyable, (2) moves are cheap, (3) the parameter is always copied. For types where move is not cheap (e.g., std::array), or for parameters that are sometimes not copied (conditional storage), the overload or universal reference approach is better. Assignment-based copying (writing into existing objects) has additional overhead from deallocation + reallocation that makes pass-by-value costlier.

Item 42: Consider emplacement instead of insertion.

Emplacement functions (emplace_back, emplace, emplace_front) forward arguments directly to element constructors — no temporary object is created or moved. Insertion functions (push_back, insert) require an object matching the container’s element type; a temporary is typically constructed then moved into the container. Emplacement wins when: (1) the value is being constructed into the container, not inserted from a pre-existing object, (2) the argument types differ from the container’s element type, (3) the container won’t reject the value as a duplicate.

Resource leak risk with emplacement: container.emplace_back(new Widget, computePriority()) can leak if computePriority() throws after new Widget but before the shared_ptr constructor runs inside the container — because emplacement uses direct initialization, not copy initialization. Fix: pass a fully-constructed shared_ptr: container.emplace_back(std::make_shared<Widget>(), computePriority()). Emplacement uses direct initialization while insertion uses copy initialization — direct initialization calls explicit constructors that copy initialization would not.


Decision Frameworks

When to use auto vs. explicit type declarations

  • Use auto when the type is obvious from the initializer, verbose to write, or known only to the compiler (closures, iterator value types).
  • Use explicit types when: the deduced type differs from what you want (proxy types — see Item 6), when documenting API contracts requires visible types, or when auto deducing std::initializer_list would be incorrect.
  • Use decltype(auto) when forwarding return types that may be references (see Item 3 authAndAccess example).

When to use unique_ptr vs. shared_ptr vs. weak_ptr

  • unique_ptr: default choice. Single owner, factory return types, Pimpl members. Converts to shared_ptr cheaply.
  • shared_ptr: multiple owners. Has 2x size overhead and atomic reference counting. Don’t use when unique_ptr suffices.
  • weak_ptr: non-owning pointer that can detect expiry. Caches, observer patterns, breaking shared_ptr cycles.
  • Never use raw pointers for ownership. Raw pointers are fine for non-owning, non-dangling access.

When to use std::move vs. std::forward

  • std::move: on rvalue reference parameters — unconditional cast to rvalue.
  • std::forward<T>: on universal reference parameters — conditional cast.
  • std::move on the last use of a local returned by value: yes, but prefer to let NRVO work.
  • std::move on a const object: silently falls back to copy. Remove const first.

When to declare noexcept

  • Move constructors and move assignment operators — required by STL containers to enable move during reallocation.
  • Swap functions — required by standard algorithms to use efficient swap.
  • Destructors — implicitly noexcept; don’t throw from them.
  • Any function you can prove won’t throw — enables compiler optimization.

Task-based vs. thread-based concurrency

  • Default: std::async (task-based). Handles thread exhaustion, oversubscription, load balancing.
  • std::thread directly: only when you need platform thread API, precise thread tuning, or thread primitives.
  • std::launch::async explicitly: when synchronous deferred execution would be wrong.

Vocabulary

lvalue: An expression whose address you can take; generally refers to a named object. Parameters are always lvalues.

rvalue: An expression whose address you cannot take; typically a temporary. std::move(x) casts x to an rvalue.

universal reference (forwarding reference): A T&& parameter in a template where T is deduced, or an auto&& variable. Binds to both lvalues and rvalues.

reference collapsing: The rule that & &, & &&, && & all collapse to &; only && && collapses to &&. Enables universal references.

perfect forwarding: Passing a parameter to another function while preserving its value category (lvalue/rvalue). Requires universal references and std::forward.

move semantics: Transfer of resource ownership from one object to another, typically O(1) for heap-owning containers. Enabled by rvalue references and move constructors/assignment operators.

control block (shared_ptr): Heap-allocated structure containing reference count, weak count, and custom deleter for a shared_ptr-managed object.

SSO (small string optimization): Storing short strings (≤15 chars typically) inline in the std::string object, avoiding heap allocation. Makes moving such strings no faster than copying.

RAII: Resource Acquisition Is Initialization. Bind resource lifetime to object lifetime; destructor performs cleanup. Smart pointers, std::lock_guard, and std::fstream are RAII types.

init capture (generalized lambda capture, C++14): [name = expr] in a lambda — allows move-initializing a closure data member from an arbitrary expression.

decltype(auto): Deduces the type using decltype rules (preserving references) rather than auto rules (stripping references).

shared state: The heap-allocated object mediating communication between a std::promise and a std::future, holding the result/exception and reference counts.


Anti-Patterns

Moving const objects: std::move(constWidget) silently copies — const rvalues bind to copy constructors, not move constructors.

Default by-reference capture with longer-lived closures: [&]{ return divisor; } when the closure outlives the local divisor — dangling reference, undefined behavior.

Default by-value capture of member data: [=]{ return divisor; } inside a member function captures this, not a copy of divisor. Object destruction → dangling pointer.

Creating two shared_ptrs from the same raw pointer: Two control blocks, two ref counts, double-destruction. Instead: use make_shared or use shared_from_this.

Checking weak_ptr::expired() then dereferencing: Race condition between check and dereference. Use lock() atomically.

Joinable std::thread exiting scope: Program terminates. Always join() or detach() before the std::thread destructor runs, including on exception paths.

Polling wait_for on a potentially-deferred task: If the default launch policy deferred the task, wait_for returns deferred forever. Check wait_for(0s) == deferred first.

std::move on returned local variables: Inhibits NRVO. Let the compiler elide the copy/move. Only use std::move on return when returning a parameter, not a local.

auto for proxy types: auto x = vec[5] where vec is std::vector<bool> — deduces a dangling proxy. Use auto x = static_cast<bool>(vec[5]).

Braced initialization with initializer_list constructors: std::vector<int> v{10, 20} creates {10, 20}, not 10 elements of value 20. The initializer_list overload dominates.

Forgetting override: Silent non-override — you have a new virtual function, not the override you intended. Every intended override should carry override.

Omitting noexcept on move constructors: STL containers copy instead of move during reallocation, negating the performance benefit of movability.

emplace_back(new T, f()) pattern: Resource leak if f() throws between new T and shared_ptr construction. Use make_shared before emplacement.

Using volatile for inter-thread synchronization: volatile provides no atomicity guarantees and does not prevent data races. Use std::atomic.

Declaring destructors in a Pimpl class header without defining them: unique_ptr<Impl> requires Impl to be complete at the point where ~unique_ptr is instantiated. Define the outer class destructor (even as = default) in the .cpp file.


How to Apply This Knowledge

When reviewing or writing C++ code, apply this skill by:

  1. Type questions: When a deduced type is surprising, trace through the three-case template deduction rules (Item 1), the braced-initializer special case (Item 2), and the lvalue-expression-in-parentheses case for decltype (Item 3).

  2. Ownership questions: Default to unique_ptr. Suggest shared_ptr only when true shared ownership exists. Flag any new/delete pairs and replace with smart pointers. Flag raw new passed directly to shared_ptr constructors (use make_shared/make_unique).

  3. Move semantics questions: Check whether move constructors are noexcept (Item 14, 29). Check whether const prevents a move (Item 23). Verify std::move vs. std::forward usage matches rvalue reference vs. universal reference (Items 25, 23).

  4. Lambda questions: Flag default captures (Items 31) and explain the this-capture trap. Suggest init capture (C++14) or explicit named captures as fixes.

  5. Concurrency questions: Suggest std::async over std::thread by default (Item 35). Flag joinable std::thread lifetime issues (Item 37). Explain std::atomic vs. volatile distinctions precisely (Item 40). Flag infinite-loop risk with default-launch async and timeout waits (Item 36).

  6. API design questions: When a class declares a destructor, recommend explicit = default for copy/move operations to re-enable them (Item 17). Recommend override on all overriding functions (Item 12). Recommend noexcept on move operations and swap (Item 14).

  7. Performance questions: Evaluate pass-by-value tradeoffs using Item 41’s cost model. Suggest emplacement when constructing directly into containers from non-element-type arguments (Item 42). Warn about std::array move being O(n) and SSO preventing string move optimization (Item 29).


Source Attribution

This skill was generated from: Effective Modern C++ by Scott Meyers

  • Publisher: O’Reilly Media (November 2014, First Edition)
  • ISBN: 978-1-491-90399-5
  • Subtitle: 42 Specific Ways to Improve Your Use of C++11 and C++14
  • Type: technical
  • Domain: C++ programming
  • Generated by: claude-code-skill (pdf-to-claude-skills)