[NOTE] Effective Modern C++

29 minute read

CH1: Deducing Types

Item 1: Understand template type deduction.

  • During template type deduction, arguments that are references are treated as non-references, i.e., their reference-ness is ignored.

  • When deducing types for universal reference parameters, lvalue arguments get special treatment.

  • When deducing types for by-value parameters, const and/or volatile arguments are treated as non-const and non-volatile.

  • During template type deduction, arguments that are array or function names decay to pointers, unless they’re used to initialize references.

Item 2: Understand auto type deduction.

  • auto type deduction is usually the same as template type deduction, but auto type deduction assumes that a braced initializer represents a std::initializer_list, and template type deduction doesn’t.

  • auto in a function return type or a lambda parameter implies template type deduction, not auto type deduction.

 1// wrong usage 1
 2auto createInitList() { return {1, 2, 3}; }
 3
 4// wrong usage 2
 5std::vector<int> v;
 6auto resetV = [&v](const auto& newValue) { v = newValue; };
 7resetV({1, 2, 3});
 8
 9// right usage
10template <typename T>
11void f(std::initializer_list<T> initList);
12f({11, 23, 9});

Item 3: Understand decltype.

  • decltype almost always yields the type of a variable or expression without any modifications.

  • For lvalue expressions of type T other than names, decltype always reports a type of T&.

  • C++14 supports decltype(auto), which, like auto, deduces a type from its initializer, but it performs the type deduction using the decltype rules.

 1// C++14 version
 2template <typename Container, typename Index>
 3decltype(auto) authAndAccess(Container&& c, Index i) {
 4  return std::forward<Container>(c)[i];  // apply std::forward to universal references
 5}
 6
 7// C++11 version
 8template <typename Container, typename Index>
 9auto authAndAccess(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i]) {
10  return std::forward<Container>(c)[i];  // apply std::forward to universal references
11}
12
13decltype(auto) f1() {
14  int x = 0;
15  return x;  // decltype(x) is int, so f1 returns int
16}
17
18decltype(auto) f1() {
19  int x = 0;
20  return (x);  // decltype((x)) is int&, so f2 returns int&
21}

Item 4: Know how to view deduced types.

  • Deduced types can often be seen using IDE editors, compiler error messages, and the Boost TypeIndex library.

  • The results of some tools may be neither helpful nor accurate, so an understanding of C++’s type deduction rules remains essential.

    • The specification for std::type_info::name mandates that the type be treated as if it had been passed to a template function as a by-value parameter.

CH2: auto

Item 5: Prefer auto to explicitly type declarations.

  • auto variables must be initialized, are generally immune to type mismatches that can lead to portability or efficiency problems, can ease the process of refactoring, and typically require less typing than variables with explicitly specified types.

  • auto-typed variables are subject to the pitfalls described in Items 2 and 6.

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

  • “Invisible” proxy types can cause auto to deduce the “wrong” type for an initializing expression.

  • The explicitly typed initializer idiom forces auto to deduce the type you want it to have.

 1class Widget;
 2std::vector<bool> features(const Widget& w);
 3void processWidget(Widget& w, bool priority);
 4
 5Widget w;
 6
 7// undefined behavior
 8// the call to features returns a temporary std::vector<bool> object and operator[] returns
 9// std::vector<bool>::reference object, which contains a pointer to a word plus the offset
10// corresponding to bit 5; highPriority is a copy of the temporary reference object and also
11// contains a pointer to a word in the above temporary reference object; at the end of the
12// statement, this pointer becomes a dangling pointer
13auto highPriority = features(w)[5];
14processWidget(w, highPriority);
15
16// the explicitly typed initializer idiom
17auto highPriority = static_cast<bool>(features(w)[5]);
18processWidget(w, highPriority);

CH3: Moving to Modern C++

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

  • Braced initialization is the most widely usable initialization syntax, it prevents narrowing conversions, and it’s immune to C++’s most vexing parse.

  • During constructor overload resolution, braced initializers are matched to std::initializer_list parameters if at all possible, even if other constructors offer seemingly better matches.

  • An example of where the choice between parentheses and braces can make a significant difference is creating a std::vector<numeric type> with two arguments.

  • Choosing between parentheses and braces for object creation inside templates can be challenging.

 1// uncopyable objects may be initialized using braces or parentheses, but not using "="
 2std::atomic<int> ai1{0};   // fine
 3std::atomic<int> ai2(0);   // fine
 4std::atomic<int> ai3 = 0;  // error!
 5
 6// prohibit implicit narrowing conversions among built-in types
 7double x, y, z;
 8int sum1{x + y + z};  // error!
 9
10// not possible to know which should be used if you're a template author
11template <typename T, typename... TS>
12void doSomeWork(Ts&&... params) {
13  // create local T object from params ...
14}
15
16T localObject(std::forward<Ts>(params)...);  // using parens
17T localObject{std::forward<Ts>(params)...};  // using braces
18
19std::vector<int> v;
20doSomeWork<std::vector<int>>(10, 20);

Item 8: Prefer nullptr to 0 and NULL.

  • Prefer nullptr to 0 and NULL.

  • Avoid overloading on integral and pointer types.

 1class Widget;
 2std::mutex f1m, f2m, f3m;
 3
 4int f1(std::shared_ptr<Widget> spw);
 5int f2(std::unique_ptr<Widget> upw);
 6bool f3(Widget* pw);
 7
 8template <typename FuncType, typename MuxType, typename PtrType>
 9decltype(auto) lockAndCall(FuncType func, MuxType& mutex, PtrType ptr) {
10  using MuxGuard = std::lock_guard(MuxType);
11  MuxGuard g(mutex);
12  return func(ptr);
13}
14
15auto result1 = lockAndCall(f1, f1m, 0);        // error!
16auto result2 = lockAndCall(f2, f2m, NULL);     // error!
17auto result3 = lockAndCall(f3, f3m, nullptr);  // fine

Item 9: Prefer alias declarations to typedefs.

  • typedefs don’t support templatization, but alias declarations do.

  • Alias templates avoid the “::type” suffix and, in templates, the “typename” prefix often required to refer to typedefs.

  • C++14 offers alias templates for all the C++11 type traits transformations.

 1class Widget;
 2
 3// use typedefs
 4template <typename T>
 5struct MyAllocList {
 6  typedef std::list<T, MyAlloc<T>> type;
 7};
 8MyAllocList<Widget>::type lw;
 9
10template <typename T>
11class Widget {
12 private:
13  typename MyAllocList<T>::type list;
14};
15
16// use alias
17template <typename T>
18using MyAllocList = std::list<T, MyAlloc<T>>;
19MyAllocList<Widget> lw;
20
21template <typename T>
22class Widget {
23 private:
24  MyAllocList<T> list;  // no "typename", no "::type"
25};

Item 10: Prefer scoped enums to unscoped enums.

  • C++98-style enums are now known as unscoped enums.

  • Enumerators of scoped enums are visible only within the enum. They convert to other types only with a cast.

  • Both scoped and unscoped enums support specification of the underlying type. The default underlying type for scoped enums is int. Unscoped enums have no default underlying type.

  • Scoped enums may always be forward-declared. Unscoped enums may be forward-declared only if their declaration specifies an underlying type.

 1using UserInfo = std::tuple<std::string, std::string, std::size_t>;
 2
 3// useful for unscoped enums
 4enum UserInfoFields { uiName, uiEmail, uiReputation };
 5UserInfo uInfo;
 6auto val = std::get<uiEmail>(uInfo);
 7
 8// pay for scoped enums
 9enum class UserInfoFields { uiName, uiEmail, uiReputation };
10UserInfo uInfo;
11auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);
12
13// std::get is a template and its argument has to be known during compilation
14// the generalized function should be a constexpr function
15template <typename E>
16constexpr typename std::underlying_type<E>::type toUType(E enumerator) noexcept {  // C++11
17  return static_cast<typename std::underlying_type<E>::type>(enumerator);
18}
19template <typename E>
20constexpr auto toUType(E enumerator) noexcept {  // C++14
21  return static_cast<std::underlying_type_t<E>>(enumerator);
22}

Item 11: Prefer deleted functions to private undefined ones.

  • Prefer deleted functions to private undefined ones.

  • Any function may be deleted, including non-member functions and template instantiations.

 1class Widget {
 2 public:
 3  template <typename T>
 4  void processPointer(T* ptr) {
 5    // ...
 6  }
 7
 8 private:
 9  template <>  // error!
10  void processPointer<void>(void*);
11};
12// template specializations must be written at namespace scope, not class scope
13template <>
14void Widget::processPointer<void>(void*) = delete;  // fine

Item 12: Declare overriding functions override.

  • Declare overriding functions override.

  • Member function reference qualifiers make it possible to treat lvalue and rvalue objects (*this) differently.

Item 13: Prefer const_iterators to iterators.

  • Prefer const_iterators to iterators.

  • In maximally generic code, prefer non-member versions of begin, end, rbegin, etc., over their member function counterparts.

1// invoking the non-member begin function on a const container yields a const_iterator and that
2// iterator is what this template returns
3template <class C>
4auto cbegin(const C& container) -> decltype(std::begin(container)) {
5  return std::begin(container);
6}

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

  • noexcept is part of a function’s interface, and that means that callers may depend on it.

  • noexcept functions are more optimizable than non-noexcept functions.

    • Optimizers need not keep the runtime stack in an unwindable state if an exception would propagate out of the function, nor must they ensure that objects in a noexcept function are destroyed in the inverse oder of construction should an exception leave the function.
  • noexcept is particularly valuable for the move operations, swap, memory deallocation functions, and destructors.

    • swap functions are conditionally noexcept and they depend on whether the expressions inside the noexcept clauses are noexcept.
    • The only time a destructor is not implicitly noexcept is when a data member of the class is of a type that expressly states that its destructor may emit exceptions.
  • Most functions are exception-neutral rather than noexcept.

Item 15: Use constexpr whenever possible.

  • constexpr objects are const and are initialized with values known during compilation.

  • constexpr functions can produce compile-time results when called with arguments whose values are known during compilation.

    • constexpr functions are limited to taking and returning literal types.
    • In C++11, constexpr functions may contain no more than a single executable statement: a return.
    • In C++11, all built-in types except void qualify, but user-defined types may be literal, too.
  • constexpr objects and functions may be used in a wider range of contexts than non-constexpr objects and functions.

  • constexpr is part of an object’s or function’s interface.

 1class Point {
 2 public:
 3  constexpr Point(double xVal = 0, double yVal = 0) noexcept : x(xVal), y(yVal) {}
 4
 5  constexpr double xValue() const noexcept { return x; }
 6  constexpr double yValue() const noexcept { return y; }
 7
 8  // C++11
 9  void setX(double newX) noexcept { x = newX; }
10  void setY(double newY) noexcept { y = newY; }
11
12  // C++14
13  constexpr void setX(double newX) noexcept { x = newX; }
14  constexpr void setY(double newY) noexcept { y = newY; }
15
16 private:
17  double x, y;
18};
19
20constexpr Point midpoint(const Point& p1, const Point& p2) noexcept {
21  return {(p1.xValue() + p2.xValue()) / 2, (p1.yValue() + p2.yValue()) / 2};
22}
23
24constexpr Point reflection(const Point& p) noexcept {
25  Point result;
26
27  // C++14 required
28  result.setX(-p.xValue());
29  result.setY(-p.yValue());
30
31  return result;
32}
33
34constexpr Point p1(9.4, 27.7);
35constexpr Point p2(28.8, 5.3);
36constexpr auto mid = midpoint(p1, p2);
37constexpr auto reflected = reflection(mid);  // C++14 required

Item 16: Make const member functions thread safe.

  • Make const member functions thread safe unless you’re certain they’ll never be used in a concurrent context.

  • Use of std::atomic variables may offer better performance than a mutex, but they’re suited for manipulation of only a single variable or memory location.

Item 17: Understand special member function generation.

  • The special member functions are those compilers may generate on their own: default constructor, destructor, copy operations, and move operations.

  • Move operations are generated only for classes lacking explicitly declared move operations, copy operations, and a destructor.

  • The copy constructor is generated only for classes lacking an explicitly declared copy constructor, and it’s deleted if a move operation is declared. The copy assignment operator is generated only for classes lacking an explicitly declared copy assignment operator, and it’s deleted if a move operation is declared. Generation of the copy operations in classes with an explicitly declared copy operation or destructor is deprecated.

  • Member function templates never suppress generation of special member functions.

 1class Base {
 2 public:
 3  virtual ~Base() = default;
 4
 5  Base(Base&&) = default;  // support moving
 6  Base& operator=(Base&&) = default;
 7
 8  Base(const Base&) = default;  // support copying
 9  Base& operator=(const Base&) = default;
10
11  // ...
12};

CH4: Smart Pointers

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

  • std::unique_ptr is a small, fast, move-only smart pointer for managing resources with exclusive-ownership semantics.

  • By default, resource destruction takes place via delete, but custom deleters can be specified. Stateful deleters and function pointers as deleters increase the size of std::unique_ptr objects.

  • Attempting to assign a raw pointer (e.g., from new) to std::unique_ptr won’t compile, because it would constitute an implicit conversion from a raw to a smart pointer. Such implicit conversions can be problematic, so C++11’s smart pointers prohibit them.

  • Converting a std::unique_ptr to a std::shared_ptr is easy.

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

  • std::shared_ptrs offer convenience approaching that of garbage collection for the shared lifetime management of arbitrary resources.

  • Compared to std::unique_ptr, std::shared_ptr objects are typically twice as big, incur overhead for control blocks, and require atomic reference count manipulations.

    • std::make_shared always creates a control block.
    • A control block is created when a std::shared_ptr is constructed from a unique-ownership pointer.
    • When a std::shared_ptr constructor is called with a raw pointer, it creates a control block.
  • Default resource destruction is via delete, but custom deleters are supported. They type of the deleter has no effect on the type of the std::shared_ptr.

  • Avoid creating std::shared_ptrs from variables of raw pointer type.

 1// a data structure keeps track of Widgets that have been processed
 2class Widget;
 3std::vector<std::shared_ptr<Widget>> processedWidgets;
 4
 5// bad example
 6class Widget {
 7 public:
 8  // ...
 9  void process();
10  // ...
11};
12
13// this is wrong, though it will compile
14// std::shared_ptr will create a new control block for the pointed-to Widget (*this)
15void Widget::process() {
16  // ...
17  processedWidgets.emplace_back(this);
18}
19
20// good example
21class Widget : public std::enable_shared_from_this<Widget> {
22 public:
23  // factory function that perfect-forwards args to a private ctor
24  template <typename... Ts>
25  static std::shared_ptr<Widget> create(Ts&&... params);
26
27  void process();
28  // ...
29
30 private:
31  // ctors
32};
33
34// shared_from_this creates a std::shared_ptr to the current object, but it does it without
35// duplicating control blocks
36// the design relies on the current object having an associated control block, thus factory
37// function is applied
38void Widget::process() {
39  // ...
40  processedWidgets.emplace_back(shared_from_this());
41}

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

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

  • Potential use cases for std::weak_ptr include caching, observer lists, and the prevention of std::shared_ptr cycles.

 1class Widget;
 2class WidgetID;
 3
 4std::unique_ptr<const Widget> loadWidget(WidgetID id);
 5
 6// the Observer design pattern
 7std::shared_ptr<const Widget> fastLoadWidget(WidgetID id) {
 8  static std::unordered_map<WidgetID, std::weak_ptr<const Widget>> cache;
 9  auto objPtr = cache[id].lock();
10  if (!objPtr) {
11    objPtr = loadWidget(id);
12    cache[id] = objPtr;
13  }
14  return objPtr;
15}

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

  • Compared to direct use of new, make functions eliminate source code duplication, improve exception safety, and, for std::make_shared and std::allocate_shared, generate code that’s smaller and faster.

  • Situations where use of make functions is inappropriate include the need to specify custom deleters and a desire to pass braced initializers.

  • For std::shared_ptrs, additional situations where make functions may be ill-advised include (1) classes with custom memory management and (2) systems with memory concerns, very large objects, and std::weak_ptrs that outlive the corresponding std::shared_ptrs.

 1class Widget;
 2
 3// advantage 1 (the same applies to std::shared_ptr)
 4auto upw1(std::make_unique<Widget>());
 5std::unique_ptr<Widget> upw2(new Widget);
 6
 7// advantage 2 (the same applies to std::shared_ptr)
 8void processWidget(std::unique_ptr<Widget> upw, int priority);
 9int computePriority();
10processWidget(std::unique_ptr<Widget>(new Widget), computePriority());  // potential resource leak!
11processWidget(std::make_unique<Widget>(), computePriority());  // no potential resource leak
12
13// advantage 3 (for std::shared_ptr only)
14std::shared_ptr<Widget> spw(new Widget);  // two allocations for Widget and control block
15auto spw = std::make_shared<Widget>();    // one allocation
16
17// disadvantage 1 (the same applies to std::shared_ptr)
18auto widgetDeleter = [](Widget* pw) {};
19std::unique_ptr<Widget, decltype(widgetDeleter)> upw(new Widget, widgetDeleter);
20
21// disadvantage 2 (the same applies to std::shared_ptr)
22auto upv = std::make_unique<std::vector<int>>(10, 20);  // apply perfect-forward and use parentheses
23auto initList = {10, 20};
24auto upv = std::make_unique<std::vector<int>>(initList);

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

  • The Pimpl Idiom decreases build times by reducing compilation dependencies between class clients and class implementations.

  • For std::unique_ptr pImpl pointers, declare special member functions in the class header, but implement them in the implementation file. Do this even if the default function implementations are acceptable.

  • The above advice applies to std::unique_ptr, but not to std::shared_ptr.

    • For std::shared_ptr, the type of the deleter is not part of the type of the smart pointer. This necessitates larger runtime data structures and somewhat slower code, but pointed-to types need not be complete when compiler-generated special functions are employed.
 1// "widget.h"
 2class Widget {
 3 public:
 4  Widget();
 5  ~Widget();
 6  Widget(Widget&& rhs) noexcept;
 7  Widget& operator=(Widget& rhs) noexcept;
 8  // ...
 9
10 private:
11  struct Impl;
12  std::unique_ptr<Impl> pImpl;
13};
14
15// "widget.cpp"
16#include <string>
17#include <vector>
18#include "gadget.h"
19#include "widget.h"
20
21struct Widget::Impl {
22  std::string name;
23  std::vector<double> data;
24  Gadget g1, g2, g3;
25};
26
27Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
28
29// (if no definition)
30// prior to using delete, however, implementations typically have the default deleter employ C++11's
31// static_assert to ensure that the raw pointer doesn't point to an incomplete type; when the
32// compiler generates code for the destruction of the Widget w, then, it generally encounters a
33// statc_assert that fails, and that's usually what leads to the error message; this message is
34// associated with the point where w is destroyed, because Widget's destructor, like all
35// compiler-generated special member functions, is implicitly inline; the message itself often
36// refers to the line where w is created, because it's the source code explicitly creating the
37// object that leads to its later implicit destruction
38Widget::~Widget() = default;
39
40// (if no definition)
41// the problem here is that compilers must be able to generate code to destroy pImpl in the event
42// that an exception arises inside the move constructor (even if the constructor is noexcept!), and
43// destroying pImpl requires that Impl be complete
44Widget::Widget(Widget&& rhs) noexcept = default;
45
46// (if no definition)
47// the compiler-generated move assignment operator needs to destroy the object pointed to by pImpl
48// before reassigning it, but in the Widget header file, pImpl points to an incomplete type
49Widget& Widget::operator=(Widget&& rhs) noexcept = default;

CH5: Rvalue References, Move semantics, and Perfect Fowarding

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

  • std::move performs an unconditional cast to an rvalue. In and of itself, it doesn’t move anything.

  • std::forward casts its argument to an rvalue only if that argument is bound to an rvalue.

  • Neither std::move or std::forward do anything at runtime.

  • Move requests on const objects are treated as copy requests.

Item 24: Distinguish universal references from rvalue references.

  • If a function template parameter has type TT& for a deduced type T, or if an object is declared using auto&&, the parameter or object is a universal reference.

  • If the form of the type declaration isn’t precisely type&&, or if type deduction does not occur, type&& denotes an rvalue reference.

  • Universal references correspond to rvalue references if they’re initialized with rvalues. They correspond to lvalue references if they’re initialized with lvalues.

 1class Widget;
 2
 3void f(Widget&& param);  // rvalue reference
 4
 5Widget&& var1 = Widget();  // rvalue reference
 6
 7auto&& var2 = var1;  // universal reference
 8
 9template <typename T>
10void f(std::vector<T>&& param);  // rvalue reference
11
12template <typename T>
13void f(T&& param);  // universal reference
14
15template <class T, class Allocator = std::allocator<T>>
16class vector {
17 public:
18  // rvalue reference
19  // there's no type deduction in this case because push_back can't exist without a particular
20  // vector instantiation for it to be part of, and the type of that instantiation fully determines
21  // the declaration for push_back
22  void push_back(T&& x);
23
24  // the type parameter Args is independent of vector's type parameter T, so Args must be deduced
25  // each time emplace_back is called
26  template <class Args>
27  void emplace_back(Args&&... args);
28
29  // ...
30};

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

  • Apply std::move to rvalue references and std::forward to universal references the last time each is used.

  • Do the same thing for rvalue references and universal references being returned from functions that return by value.

  • Never apply std::move or std::forward to local objects if they would otherwise be eligible for the return value optimization.

    • The compilers may elide the copying (or moving) of a local object in a function that returns by value if (1) the type of the local object is the same as that returned by the function and (2) the local object is what’s being returned.
    • If the conditions for the RVO are met, but compilers choose not to perform copy elision, the object being returned must be treated as an rvalue.
 1// in a function that returns by value and returns an object bound to an rvalue reference or a
 2// universal reference, apply std::move or std::forward when you return the reference
 3Matrix operator+(Matrix&& lhs, const Matrix& rhs) {
 4  lhs += rhs;
 5  return std::std::move(lhs);  // move lhs into return value
 6}
 7
 8template <typename T>
 9Fraction reduceAndCopy(T&& frac) {
10  frac.reduce();
11  return std::forward<T>(frac);
12}

Item 26: Avoid overloading on universal references.

  • Overloading on universal references almost always leads to the universal reference overload being called more frequently than expceted.

  • Perfect-forwarding constructors are especially problematic, because they’re typically better matches than copy constructors for non-const lvalues, and they can hijack derived class calls to base class copy and move constructors.

 1std::string nameFromIdx(int idx);
 2
 3class Person {
 4 public:
 5  template <typename T>
 6  explicit Person(T&& n) : name(std::forward<T>(n)) {}
 7
 8  explicit Person(int idx) : name(nameFromIdx(idx)) {}
 9
10  Person(const Person& rhs);  // compiler-generated
11
12  Person(Person&& rhs);  // compiler-generated
13
14 private:
15  std::string name;
16};
17
18// compiler is being initialized with a non-const lvalue (p), and that means that the templatized
19// constructor can be instantiated to take a non-const lvalue of type Person
20// explicit Person(Person& n) : name(std::forward<Person>(n)) {}
21// calling the copy constructor would require adding const to p to match the copy constructor's
22// parameter's type
23Person p("Nancy");
24auto cleanOfP(p);  // this won't compile!
25
26// situations where a template instantiation and a non-template function are equally good matches
27// for a function call, the normal function is preferred
28const Person p("Nancy");
29auto cleanOfP(p);  // call copy constructor
30
31// these two constructors will call base class forwarding constructor because the derived class
32// functions are using arguments of type SpecialPerson to pass to their base class
33class SpecialPerson : public Person {
34 public:
35  SpecialPerson(const SpecialPerson& rhs) : Person(rhs) {}
36  SpecialPerson(SpecialPerson&& rhs) : Person(std::move(rhs)) {}
37};

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

  • Alternatives to the combination of universal references and overloading include the use of distinct function names, passing parameters by lvalue-reference-to-const, passing parameters by value, and using tag dispatch.

  • Constraining templates via std::enable_if permits the use of universal references and overloading together, but it controls the conditions under which compilers may use the universal reference overloads.

  • Universal reference parameters often have efficiency advantages, but they typically have usability disadvantages.

 1// use tag dispatch
 2template <typename T>
 3void logAndAdd(T&& name) {
 4  logAndAddImpl(std::forward<T>(name), std::is_integral<std::remove_reference_t<T>::type>());
 5}
 6
 7template <typename T>
 8void logAndAddImpl(T&& name, std::false_type) {
 9  auto now = std::chrono::system_clock::now();
10  log(now, "logAndAdd");
11  names.emplace(std::forward<T>(name));
12}
13
14void logAndAddImpl(int idx, std::true_type) { logAndAdd(nameFromIdx(idx)); }
15
16// constrain templates that take universal references
17class Person {
18 public:
19  template <typename T,
20            typename = std::enable_if_t<!std::is_base_of<Person, std::decay_t<T>>::value &&
21                                        !std::is_integral<std::remove_reference_t<T>>::value>>
22  explicit Person(T&& n) : name(std::forward<T>(n)) {
23    // assert that a std::string can be created from a T object
24    static_assert(std::is_constructible<std::string, T>::value,
25                  "Parameter n can't be used to construct a std::string");
26
27    // the usual constructor work goes here
28  }
29
30  // remainder of Person class
31};

Item 28: Understand reference collapsing.

  • Reference collapsing occurs in four contexts: template instantiation, auto type generation, creation and use of typedefs and alias declarations, and decltype.

  • When compilers generate a reference to a reference in a reference collapsing context, the result becomes a single reference. If either the original references is an lvalue reference, the result is an lvalue reference. Otherwise it’s an rvalue reference.

  • Universal references are rvalue references in contexts where type deduction distinguishes lvalues from rvalues and where reference collapsing occurs.

1template <typename T>
2T&& forward(std::remove_reference_t<T>& param) {
3  return static_assert<T&&>(param);
4}

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

  • Assume that move operations are not present, not cheap, and not used.

    • No move operations: The object to be moved from fails to offer move operations. The move request therefore becomes a copy request.
    • Move not fast: The object to be moved from has move operations that are no faster than its copy operations.
    • Move not usable: The context in which the moving would take place requires a move operation that emits no exceptions, but that operation isn’t declared noexcept.
    • Source object is lvalue: With very few exceptions only rvalues may be used as the source of a move operation.
  • In code with known types or support for move semantics, there is no need for assumptions.

Item 30: Familiarize yourself with perfect forwarding failure cases.

  • Perfect forwarding fails when template type deduction fails or when it deduces the wrong type.

  • The kinds of arguments that lead to perfect forwarding failure are braced initializers, null pointers expressed as 0 or NULL, declaration-only integral const static data members, template and overloaded function names, and bitfields.

 1template <typename... Ts>
 2void fwd(Ts&&... params) {
 3  f(std::forward<Ts>(params)...);
 4}
 5
 6// failure case 1: braced initializers
 7// the problem is that passing a braced initializer to a function template parameter that's not
 8// declared to be a std::initializer_list is decreed to be, as the Standard puts it, a "non-deduced
 9// context"
10void f(const std::vector<int>& v);
11f({1, 2, 3});  // fine
12
13fwd({1, 2, 3});  // error!
14
15auto il = {1, 2, 3};
16fwd(il);  // fine
17
18// failure case 2: 0 or NULL as null pointers
19// the problem is that neither 0 or NULL can be perfect-forwarded as a null pointer
20
21// failure case 3: declaration-only integral static const and constexpr data members
22// the problem is that compilers perform const propagation on such members' values, thus eliminating
23// the need to set aside memory for them and references are simply pointers that are automatically
24// dereferenced
25class Widget {
26 public:
27  static constexpr std::size_t MinVals = 28;
28  // ...
29};
30
31std::vector<int> widgetData;
32widgetData.reserve(Widget::MinVals);  // error´╝ü shouldn't link
33
34constexpr std::size_t Widget::MinVals;  // better to provide a definition
35
36// failure case 4: overloaded function names and template names
37// the problem is that a function template doesn't represent one function, it represents many
38// functions
39void f(int (*pf)(int));
40void f(int pf(int));
41
42int processVal(int value);
43int processVal(int value, int priority);
44
45f(processVal);   // fine
46fwd(processVal)  // error! which processVal?
47
48    template <typename T>
49    T workOnVal(T param) {}
50
51fwd(workOnVal);  // error! which workOnVal instantiation?
52
53using ProcessFuncType = int (*)(int);
54ProcessFuncType processValPtr = processVal;
55fwd(processValPtr);                          // fine
56fwd(static_cast<processValPtr>(workOnVal));  // also fine
57
58// failure case 5: bitfields
59// the problem is that a non-const reference shall not be bound to a bit-field and
60// reference-to-const don't bind to bitfields, they bind to "normal" objects (e.g., int) into which
61// the values of the bitfields have been copied
62struct IPv4Header {
63  std::uint32_t version : 4, IHL : 4, DSCP : 6, ECN : 2, totalLength : 16;
64};
65
66void f(std::size_t sz);
67
68IPv4Header h;
69f(h.totalLength);    // fine
70fwd(h.totalLength);  // error!
71
72auto length = static_cast<std::uint16_t>(h.totalLength);
73fwd(length);  // fine

CH6: Lambda Expressions

Item 31: Avoid default capture modes.

  • Default by-reference capture can lead to dangling references.

  • Default by-value capture is susceptible to dangling pointers (especially this), and it misleadingly suggests that lambdas are self-contained.

  • Lambdas may also be dependent on objects with static storage duration that are defined at global or namespace scope or are declared static inside classes, functions, or files. These objects can be used inside lambdas, but they can’t be captured.

 1using FilterContainer = std::vector<std::function<bool(int)>>;
 2FilterContainer filters;
 3
 4// by-reference capture issue
 5void addDivisorFilter() {
 6  auto calc1 = computeSomeValue1();
 7  auto calc2 = computeSomeValue2();
 8
 9  auto divisor = computeDivisor(calc1, calc2);
10
11  filters.emplace_back([&](int value) { return value % divisor == 0; });
12}
13
14// by-value capture issue
15class Widget {
16  void addFilter() const;
17
18 private:
19  int divisor;
20};
21
22// captures apply only to non-static local variables (including parameters) visible in the scope
23// where the lambda is created
24void Widget::addFilter() const {
25  filters.emplace_back([=](int value) { return value % divisor == 0; });
26}
27void Widget::addFilter() const {
28  auto currentObjectPtr = this;
29  filters.emplace_back(
30      [currentObjectPtr](int value) { return value % currentObjectPtr->divisor == 0; });
31}
32
33void doSomeWork() {
34  auto pw = std::make_unique<Widget>();
35  pw->addFilter();  // undefined behavior when funtion returned
36}
37
38// solution: use generalized lambda capture (C++14)
39void Widget::addFilter() const {
40  filters.emplace_back([divisor = divisor](int value) { return value % divisor == 0; });
41}

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

  • Use C++14’s init capture to move objects into closures.

  • In C++11, emulate init capture via hand-written classes or std::bind.

    • It’s not possible to move=construct an object into a C++1 closure, but it is possible to move-construct an object into a C++11 bind object.
    • Emulating move-capture in C++11 consists of move-constructing an object into a bind object, then passing the move-constructed object to the lambda by reference.
    • Because the lifetime of the bind object is the same as that of the closure, it’s possible to treat objects in the bind object as if they were in the closure.
 1class Widget {
 2 public:
 3  // ...
 4  bool isValidated() const;
 5  bool isProcessed() const;
 6  bool isArchived() const;
 7
 8 private:
 9  // ...
10};
11
12// C++11
13auto func = std::bind(
14    [](const std::unique_ptr<Widget>& pw) { return pw->isValidated() && pw->isArchived(); },
15    std::make_unique<Widget>());
16
17// C++14
18auto func = [pw = std::make_unique<Widget>()] { return pw->isValidated() && pw->isArchived(); };

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

  • Use decltype on auto&& parameters to std::forward them.
 1// the only thing the lambda does with its parameter x is forward it to normalize
 2auto f = [](auto x) { return normalize(x); };
 3
 4class SomeCompilerGeneratedClassName {
 5 public:
 6  template <typename T>
 7  auto operator()(T x) const {
 8    return normalize(x);
 9  }
10};
11
12// more elegant style
13auto f = [](auto&& x) { return normalize(std::forward<decltype(x)>(x)); };
14
15// more more elegant style
16auto f = [](auto&&... xs) { return normalize(std::forward<decltype(xs)>(xs)...); };

Item 34: Prefer lambdas to std::bind.

  • Lambdas are more readable, more expressive, and may be more efficient than using std::bind.

  • In C++11 only, std::bind may be useful for implementing move capture or for binding objects with templatized function call operators.

  • std::bind always copies its arguments, but callers can achieve the effect of having an argument stored by reference by applying std::ref to it. All arguments passed to bind objects are passed by reference, because the function call operator for such objects uses perfect forwarding.

 1// polymorphic function objects
 2class PolyWidget {
 3 public:
 4  template <typename T>
 5  void operator()(const T& param) const;
 6  // ...
 7};
 8
 9PolyWidget pw;
10auto boundPW = std::bind(pw, std::placeholders::_1);
11
12boundPW(1930);
13boundPW(nullptr);
14boundPW("Rosebud");

CH7: The Concurrency API

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

  • The std::thread API offers no direct way to get return values from asynchronously run functions, and if those functions throw, the program is terminated.

  • Thread-based programming calls for manual management of thread exhaustion, oversubscription, load balancing, and adaptation to new platforms.

  • Task-based programming via std::async with the default launch policy handles most of these issues for you.

  • There are some situations where using threads directly may be appropriate, though these are uncommon cases.

    • You need access to the API of the underlying threading implementation.
    • You need to and are able to optimize thread usage for your application.
    • You need to implement threading technology beyond the C++ concurrency API.

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

  • The default launch policy for std::async permits both asynchronous and synchronous task execution.

  • This flexibility leads to uncertainty when accessing thread_locals, implies that the task may never execute, and affects program logic for timeout-based wait calls.

  • Specify std::launch::async if asynchronous task execution is essential.

 1using namespace std::literals;
 2
 3void f() { std::this_thread::sleep_for(1s); }
 4
 5// two calls below are the same
 6auto fut1 = std::async(f);
 7auto fut2 = std::async(std::launch::async | std::launch::deferred, f);
 8
 9// if f is deferred, fut.wait_for will always return std::future_status::deferred, so the loop will
10// never terminate
11auto fut = std::async(f);
12while (fut.wait_for(100ms) != std::future_status::ready) {
13  // ...
14}
15
16// fix the issue
17auto fut = std::async(f);
18if (fut.wait_for(0s) == std::future_status::deferred) {
19  // ...
20} else {
21  while (fut.wait_for(100ms) != std::future_status::ready) {
22    // task is neither deferred nor ready,
23    // so do concurrent work until it's ready
24  }
25  // fut is ready
26}
27
28// C++11 style
29template <typename F, typename... Ts>
30inline std::future<typename std::result_of<F(Ts...)>::type> reallyAsync(F&& f, Ts&&... params) {
31  return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
32}
33
34// C++14 style
35template <typename F, typename... Ts>
36inline auto reallyAsync(F&& f, Ts&&... params) {
37  return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
38}

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

  • Make std::threads unjoinable on all paths.

  • join-on-destruction can lead to difficult-to-debug performance anomalies.

  • detach-on-destruction can lead to difficult-to-debug undefined behavior.

  • Declare std::thread objects last in lists of data members.

 1constexpr auto tenMillion = 10000000;
 2
 3bool doWork(std::function<bool(int)> filter, int maxVal = tenMillion) {
 4  std::vector<int> goodVals;
 5  std::thread t([&filter, maxVal, &goodVals] {
 6    for (auto i = 0; i <= maxVal; ++i) {
 7      if (filter(i)) {
 8        goodVals.push_back(i);
 9      }
10    }
11  });
12
13  auto nh = t.native_handle();
14
15  if (conditionsAreSatisfied()) {
16    t.join();
17    performComputation(goodVals);
18    return true;
19  }
20
21  return false;
22}
23
24class ThreadRAII {
25 public:
26  enum class DtorAction { join, detach };
27
28  ThreadRAII(std::thread&& t, DtorAction a) : action(a), t(std::move(t)) {}
29
30  ~ThreadRAII() {
31    if (t.joinable()) {
32      if (action == DtorAction::join) {
33        t.join();
34      } else {
35        t.detach();
36      }
37    }
38  }
39
40  ThreadRAII(ThreadRAII&&) = default;
41  ThreadRAII& operator=(ThreadRAII&&) = default;
42
43  std::thread& get() { return t; }
44
45 private:
46  DtorAction action;
47  std::thread t;
48};
49
50// then use RAII class to rewrite doWork()

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

  • Future destructors normally just destroy the future’s data members.

  • The final future referring to a shared state for a non-deferred task launched via std::async blocks until the task completes.

 1int calcValue();
 2
 3// a std::packaged_task object prepares a function (or other callable object) for asynchronous
 4// execution by wrapping it such that its result is put into a shared object
 5std::packaged_task<int()> pt(calcValue);
 6
 7auto fut = pt.get_future();  // get future for pt
 8
 9std::thread t(std::move(pt));  // run pt on t
10
11// client's decision
12// the decision among termination, joining, or detaching will be made in the code that manipulates
13// the std::thread on which the std::packaged_task is typically run

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

  • For simple event communication, condvar-based designs require a superfluous mutex, impose constraints on the relative progress of detecting and reacting tasks, and require reacting tasks to verify that the event has taken place.

  • Designs employing a flag avoid those problems, but are based on polling, not blocking.

  • A condvar and flag can be used together, but the resulting communications mechanism is somewhat stilted.

  • Using std::promises and futures dodges these issues, but the approach uses heap memory for shared states, and it’s limited to one-shot communication.

 1std::promise<void> p;
 2
 3void react();
 4
 5void detect() {
 6  auto sf = p.get_future().share();
 7
 8  std::vector<std::thread> vt;
 9
10  for (int i = 0; i < threadsToRun; ++i) {
11    vt.emplace_back([sf] {
12      sf.wait();
13      react();
14    });
15  }
16
17  // ThreadRAII not used, so program is terminated if code here throws!
18
19  p.set_value();
20
21  for (auto& t : vt) {
22    t.join();
23  }
24}

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

  • std::atomic is for data accessed from multiple threads without using mutexes. It’s a tool for writing concurrent software.

  • volatile is for memory where reads and writes should not be optimized away. It’s a tool for working with special memory.

CH8: Tweaks

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

  • For copyable, cheap-to-move parameters that are always copied, pass by value may be nearly as efficient as pass by reference, it’s easier to implement, and it can generate less object code.

  • For lvalue arguments, pass by value (i.e., copy construction) followed by move assignment may be significantly more expensive than pass by reference followed by copy assignment.

  • Pass by value is subject to the slicing problem, so it’s typically inappropriate for base class parameter types.

Item 42: Consider emplacement instead of insertion.

  • In principle, emplacement functions should sometimes be more efficient than their insertion counterparts, and they should never be less efficient.

  • In practice, they’re most likely to be faster when (1) the value being added is constructed into the container, not assigned; (2) the argument type(s) passed differ from the type held by the container; and (3) the container won’t reject the value being added due to it being a duplicate.

  • Emplacement functions may perform type conversions that would be rejected by insertion functions.