EMC Reading Notes - yszheda/wiki GitHub Wiki

Deducing Types

Item 1: Understand template type deduction.

Case 1: ParamType is a Reference or Pointer, but not a Universal Reference

  1. If expr’s type is a reference, ignore the reference part.
  2. Then pattern-match expr’s type against ParamType to determine T.

Case 2: ParamType is a Universal Reference

  • If expr is an lvalue, both T and ParamType are deduced to be lvalue references. That’s doubly unusual. First, it’s the only situation in template type deduction where T is deduced to be a reference. Second, although ParamType is declared using the syntax for an rvalue reference, its deduced type is an lvalue reference.
  • If expr is an rvalue, the “normal” (i.e., Case 1) rules apply.

Case 3: ParamType is Neither a Pointer nor a Reference

passed by value


Things to Remember

  • 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.

Things to Remember

  • 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. (C++17有变化)
  • auto in a function return type or a lambda parameter implies template type deduction, not auto type deduction.

Item 3: Understand decltype.

Widget w;
const Widget& cw = w;
// auto type deduction: myWidget1's type is Widget
auto myWidget1 = cw;
// decltype type deduction: myWidget2's type is const Widget&
decltype(auto) myWidget2 = cw;
  • For lvalue expressions of type T other than names, decltype always reports a type of T&.
// decltype(x) is int, so f1 returns int
decltype(auto) f1() {
    int x = 0;
    return x;
}

// decltype((x)) is int&, so f2 returns int&
decltype(auto) f2() {
    int x = 0;
    return (x);
}

-- TODO: std::forward, universal reference

Item 4: Know how to view deduced types.

auto

Item 5: Prefer auto to explicit type declarations.

The advantages of auto extend beyond the avoidance of uninitialized variables, verbose variable declarations, and the ability to directly hold closures.

// TODO

std::unordered_map<std::string, int> m;
for (const std::pair<std::string, int>& p : m)
{
    // do something with p
}

the type of std::pair in the hash table (which is what a std::unordered_map is) isn’t std::pair<std::string, int>, it’s std::pair <const std::string, int>. 类型转换的临时变量p

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

operator[] for std::vector<bool> returns an object of type std::vector<bool>::reference (a class nested inside std::vector<bool>).

   Widget w; ...
   bool highPriority = features(w)[5];  // is w high priority?
   ...
   processWidget(w, highPriority);      // process w in accord
                                        // with its priority
  auto highPriority = features(w)[5]; // is w high priority?

  processWidget(w, highPriority);      // undefined behavior!

proxy class: a class that exists for the purpose of emulating and augmenting the behavior of some other type.

  • apparent to clients: std::shared_ptr, std::unique_ptr
  • “invisible”: std::vector<bool>::reference
  • As a general rule, “invisible” proxy classes don’t play well with auto.

  • • “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. (std::static_cast)

Moving to Modern C++

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

  • uniform initialization / braced initialization

  • Braces can also be used to specify default initialization values for non-static data members.

class Widget {
    private:
        int x{ 0 };     // fine, x's default value is 0
        int y = 0;      // also fine
        int z(0);       // error!
};
  • A novel feature of braced initialization is that it prohibits implicit narrowing conversions among built-in types.
double x, y, z;
int sum1{ x + y + z }; // error! sum of doubles may not be expressible as int
int sum2(x + y + z);   // okay (value of expression truncated to an int)
int sum3 = x + y + z;  // ditto
  • Another noteworthy characteristic of braced initialization is its immunity to C++’s most vexing parse.
Widget w2(); // most vexing parse! declares a function named w2 that returns a Widget!
Widget w3{}; // calls Widget ctor with no args

  • If one or more constructors declare a parameter of type std::initializer_list, calls using the braced initialization syntax strongly prefer the overloads taking std::initializer_lists.
class Widget {
    public:
        Widget(int i, bool b);
        Widget(int i, double d);
        Widget(std::initializer_list<long double> il);

        operator float() const;
};

// uses parens and, as before,
// calls first ctor
Widget w1(10, true);

// uses braces, but now calls
// std::initializer_list ctor
// (10 and true convert to long double)
Widget w2{10, true};

// uses parens and, as before,
// calls second ctor
Widget w3(10, 5.0);

// uses braces, but now calls
// std::initializer_list ctor
// (10 and 5.0 convert to long double)
Widget w4{10, 5.0};

// uses parens, calls copy ctor
Widget w5(w4);

// uses braces, calls
// std::initializer_list ctor
// (w4 converts to float, and float converts to long double)
Widget w6{w4};

// uses parens, calls move ctor
Widget w7(std::move(w4));

// uses braces, calls
// std::initializer_list ctor
// (for same reason as w6)
Widget w8{std::move(w4)};
  • Compilers’ determination to match braced initializers with constructors taking std::initializer_lists is so strong, it prevails even if the best-match std::initializer_list constructor can’t be called.
class Widget {
   public:
     Widget(int i, bool b);                   // as before
     Widget(int i, double d);                 // as before

     Widget(std::initializer_list<bool> il); // element type is now bool

                                             // no implicit
                                             // conversion funcs
};

Widget w{10, 5.0}; // error! requires narrowing conversions
  • Only if there’s no way to convert the types of the arguments in a braced initializer to the type in a std::initializer_list do compilers fall back on normal overload resolution.
class Widget {
    public:
        Widget(int i, bool b);               // as before
        Widget(int i, double d);             // as before

        // std::initializer_list element type is now std::string
        Widget(std::initializer_list<std::string> il);

        // no implicit
        // conversion funcs
};

// uses parens, still calls first ctor
Widget w1(10, true);

// uses braces, now calls first ctor
Widget w2{10, true};

// uses parens, still calls second ctor
Widget w3(10, 5.0);

// uses braces, now calls second ctor
Widget w4{10, 5.0};
  • Empty braces mean no arguments, not an empty std::initializer_list:
class Widget {
    public:
        Widget();                               // default ctor

        Widget(std::initializer_list<int> il);  // std::initializer
                                                // _list ctor

        // no implicit
        // conversion funcs
};

// calls default ctor
Widget w1;

// also calls default ctor
Widget w2{};

// most vexing parse! declares a function!
Widget w3();

// calls std::initializer_list ctor with empty list
Widget w4({});

// ditto
Widget w5{{}};
std::vector<int> v1(10, 20); // use non-std::initializer_list
                             // ctor: create 10-element
                             // std::vector, all elements have
                             // value of 20

std::vector<int> v2{10, 20}; // use std::initializer_list ctor:
                             // create 2-element std::vector,
                             // element values are 10 and 20
  • std::make_unique and std::make_shared resolve the problem by internally using parentheses and by documenting this decision as part of their interfaces. (Intuitive interface — Part I)

Item 8: Prefer nullptr to 0 and NULL.

  • 0和NULL是int,会导致编译器选择错误的重载函数。

  • • Prefer nullptr to 0 and NULL.
  • • Avoid overloading on integral and pointer types.

Item 9: Prefer alias declarations to typedefs.

std::remove_const<T>::type          // yields T from const T

std::remove_reference<T>::type      // yields T from T& and T&&

std::add_lvalue_reference<T>::type  // yields T& from T

std::remove_const<T>::type          // C++11: const T -> T
std::remove_const_t<T>              // C++14 equivalent

std::remove_reference<T>::type      // C++11: T&/T&& -> T
std::remove_reference_t<T>          // C++14 equivalent

std::add_lvalue_reference<T>::type  // C++11: T to T&
std::add_lvalue_reference_t<T>      // C++14 equivalent

C++11 version:

template <class T>
using remove_const_t = typename remove_const<T>::type;

template <class T>
using remove_reference_t = typename remove_reference<T>::type;

template <class T>
using add_lvalue_reference_t = typename add_lvalue_reference<T>::type;

  • • 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.

Item 10: Prefer scoped enums to unscoped enums.

  1. scoped:
enum Color { black, white, red }; // black, white, red are in same scope as Color
auto white = false;               // error! white already declared in this scope
  1. Scoped enumerators are much more strongly typed.
  2. Scoped enums may be forward-declared, i.e., their names may be declared without specifying their enumerators (no recompilation if the definition is revised):
enum Color;               // error!
enum class Color;         // fine

To make efficient use of memory, compilers often want to choose the smallest under‐ lying type for an enum that’s sufficient to represent its range of enumerator values. In some cases, compilers will optimize for speed instead of size, and in that case, they may not choose the smallest permissible underlying type, but they certainly want to be able to optimize for size. To make that possible, C++98 supports only enum definitions (where all enumerators are listed); enum declarations are not allowed.

using UserInfo =                 // type alias
std::tuple<std::string,        // name
    std::string,        // email
    std::size_t> ;      // reputation

// Unscoped enum
enum UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
auto val = std::get<uiEmail>(uInfo); // ah, get value of email field

// Scoped enum: way #1
enum class UserInfoFields { uiName, uiEmail, uiReputation };
UserInfo uInfo;
auto val = std::get<static_cast<std::size_t>(UserInfoFields::uiEmail)>(uInfo);

// Scoped enum: way #2, C++11
template<typename E>
constexpr typename std::underlying_type<E>::type
toUType(E enumerator) noexcept
{
    return
        static_cast<typename
        std::underlying_type<E>::type>(enumerator);
}
auto val = std::get<toUType(UserInfoFields::uiEmail)>(uInfo);

// Scoped enum: way #2, C++14
template<typename E> // C++14 constexpr std::underlying_type_t<E>
toUType(E enumerator) noexcept
{
    return static_cast<std::underlying_type_t<E>>(enumerator);
}

// Scoped enum: way #2, C++14
template<typename E> // C++14 constexpr auto
toUType(E enumerator) noexcept
{
    return static_cast<std::underlying_type_t<E>>(enumerator);
}

  • • 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.

Item 11: Prefer deleted functions to private undefined ones.

if you have a function template inside a class, and you can’t disable some instantiations by declaring them private (à la classic C++98 convention), because it’s not possible to give a member function template specialization a different access level from that of the main template.


  • Prefer deleted functions to private undefined ones.
  • Any function may be deleted, including non-member functions and template instantiations.

Item 12: Declare overriding functions override.

For overriding to occur, several requirements must be met:

  • The base class function must be virtual.
  • The base and derived function names must be identical (except in the case of destructors).
  • The parameter types of the base and derived functions must be identical. The constness of the base and derived functions must be identical.
  • The return types and exception specifications of the base and derived functions must be compatible.
  • The functions’ reference qualifiers must be identical.
class Widget {
    public:
        using DataType = std::vector<double>;

        // for lvalue Widgets,
        // return lvalue
        DataType& data() & { return values; }

        // for rvalue Widgets,
        // return rvalue
        DataType data() &&
        { return std::move(values); }

    private:
        DataType values;
};

// calls lvalue overload for Widget::data, copy-constructs vals1
auto vals1 = w.data();

// calls rvalue overload for Widget::data, move-constructs vals2
auto vals2 = makeWidget().data();

Item 13: Prefer const_iterators to iterators.

// in container, find first occurrence
// of targetVal, then insert insertVal there
//
// It works in C++14, but not in C++11
template<typename C, typename V>
void findAndInsert(C& container,
        const V& targetVal,
        const V& insertVal)
{
    using std::cbegin;
    using std::cend;

    auto it = std::find(cbegin(container), // non-member cbegin
                        cend(container),   // non-member cend
                        targetVal);
    container.insert(it, insertVal);
}
// Implementation of non-member cbegin
// 参数的const C&是确保const_iterator的关键!

template <class C>
auto cbegin(const C& container)->decltype(std::begin(container))
{
    return std::begin(container);
}

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

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

But there’s an additional incentive to apply noexcept to functions that won’t produce exceptions: it permits compilers to generate better object code.

int f(int x) throw(); // no exceptions from f: C++98 style
int f(int x) noexcept; // no exceptions from f: C++11 style

If, at runtime, an exception leaves f, f’s exception specification is violated. With the C++98 exception specification, the call stack is unwound to f’s caller, and, after some actions not relevant here, program execution is terminated. With the C++11 exception specification, runtime behavior is slightly different: the stack is only possibly unwound before program execution is terminated. The difference between unwinding the call stack and possibly unwinding it has a surprisingly large impact on code generation. In a noexcept function, 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 order of construction should an exception leave the function.

std::vector::push_back, std::vec tor::reserve, std::deque::insert, etc. replace calls to copy operations in C++98 with calls to move operations in C++11 only if the move operations are known to not emit exceptions. (保证异常发生时容器的状态不受影响)

The fact of the matter is that most functions are exception-neutral. Such functions throw no exceptions themselves, but functions they call might emit one. When that happens, the exception-neutral function allows the emitted exception to pass through on its way to a handler further up the call chain. Exception-neutral functions are never noexcept, because they may emit such “just passing through” exceptions. Most functions, therefore, quite properly lack the noexcept designation.

A function with a wide contract has no preconditions. Such a function may be called regardless of the state of the pro‐ gram, and it imposes no constraints on the arguments that callers pass it. Functions with wide contracts never exhibit undefined behavior. Functions without wide contracts have narrow contracts. For such functions, if a precondition is violated, results are undefined.


  • • 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.
  • • noexcept is particularly valuable for the move operations, swap, memory deallocation functions, and destructors.
  • • Most functions are exception-neutral rather than noexcept.

Item 15: Use constexpr whenever possible.

constexpr functions produce compile-time constants when they are called with compile-time constants.

  • constexpr functions can be used in contexts that demand compile-time constants. If the values of the arguments you pass to a constexpr function in such a context are known during compilation, the result will be computed during compilation. If any of the arguments’ values is not known during compilation, your code will be rejected.
  • When a constexpr function is called with one or more values that are not known during compilation, it acts like a normal function, computing its result at runtime. This means you don’t need two functions to perform the same operation, one for compile-time constants and one for all other values. The constexpr function does it all.

  • 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 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.

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.

SmartPtr

  • Prefer unique_ptr to auto_ptr:

C++98 didn’t have move semantics. As a workaround, std::auto_ptr co-opted its copy operations for moves. This led to surprising code (copying a std::auto_ptr sets it to null!) and frustrating usage restrictions (e.g., it’s not possible to store std::auto_ptrs in containers).

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

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

  • shared_ptr cost:

    • std::shared_ptrs are twice the size of a raw pointer: control block
      • Reference Count
      • Weak Count
      • Other Data (e.g., custom deleter, allocator, etc.)
    • Memory for the reference count must be dynamically allocated.
    • Increments and decrements of the reference count must be atomic.
  • For std::unique_ptr, the type of the deleter is part of the type of the smart pointer. For std::shared_ptr, it’s not.

  • control block:

    • std::make_shared (see Item 21) always creates a control block.
    • A control block is created when a std::shared_ptr is constructed from a unique-ownership pointer (i.e., a std::unique_ptr or std::auto_ptr).
    • When a std::shared_ptr constructor is called with a raw pointer,it creates a control block.
  • std::enable_shared_from_this:

    • std::enable_shared_from_this defines a member function that creates a std::shared_ptr to the current object, but it does it without duplicating control blocks.

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

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

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

Rvalue References, Move Semantics, and Perfect Forwarding

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

First, don’t declare objects const if you want to be able to move from them. Move requests on const objects are silently transformed into copy operations. Second, std::move not only doesn’t actually move anything, it doesn’t even guarantee that the object it’s casting will be eligible to be moved. The only thing you know for sure about the result of applying std::move to an object is that it’s an rvalue.


Things to Remember:

  • 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 nor std::forward do anything at runtime.

Item 24: Distinguish universal references from rvalue references.

  • universal references: type deduction
    • function template parameters
    • auto declarations
template<class T, class Allocator = allocator<T>>  // from C++
class vector {                                     // Standards
    public:
        void push_back(T&& x);                     // rvalue reference
};
template<class T, class Allocator = allocator<T>>  // still from
class vector {                                     // C++ Standards
    public:
        template <class... Args>
            void emplace_back(Args&&... args);
};

  • If a function template parameter has type T&& 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.

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


compiler optimization:

Item 26: Avoid overloading on universal references.


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

Use Tag dispatch

   template<typename T>
   void logAndAdd(T&& name)
   {
logAndAddImpl(
std::forward<T>(name),
std::is_integral<typename std::remove_reference<T>::type>()
); }

Constraining templates that take universal references

std::enable_if: SFINAE

// TODO

Item 28: Understand reference collapsing.

When an lvalue is passed as an argument, T is deduced to be an lvalue reference. When an rvalue is passed, T is deduced to be a non-reference.

If a reference to a reference arises in a context where this is permitted (e.g., during template instantiation), the references collapse to a single reference according to this rule: If either reference is an lvalue reference, the result is an lvalue reference. Otherwise (i.e., if both are rvalue references) the result is an rvalue reference.

Reference collapsing occurs in four contexts:

  • The first and most common is template instantiation.
  • The second is type generation for auto variables.
  • The third is the generation and use of typedefs and alias declarations.
  • The final context in which reference collapsing takes place is uses of decltype.

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

There are thus several scenarios in which C++11’s move semantics do you no good: • No move operations: The object to be moved from fails to offer move operations. The move request therefore becomes a copy request. • Move not faster: 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. It’s worth mentioning, too, another scenario where move semantics offers no efficiency gain: • Source object is lvalue: With very few exceptions (see e.g., Item 25) only rvalues may be used as the source of a move operation.

std::array会线性move所有元素,而不是如std::vector一样只move buffer。

std::string如果有small string optimization,copy可能比move更快。

Item 30: Familiarize yourself with perfect forwarding failure cases.

variadic templates

template<typename... Ts>
void fwd(Ts&&... params) // accept any arguments
{
f(std::forward<Ts>(params)...); // forward them to f
}

Braced initializers

0 or NULL as null pointers

Declaration-only integral static const data members

Overloaded function names and template names

Bitfields

Lambda

  • A closure is the runtime object created by a lambda.
  • A closure class is a class from which a closure is instantiated. Each lambda causes compilers to generate a unique closure class. The statements inside a lambda become executable instructions in the member functions of its closure class.

Item 31: Avoid default capture modes.

  • Captures apply only to non-static local variables (including parameters) visible in the scope where the lambda is created.

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

// C++14

std::vector<double> data;
...
auto func = [data = std::move(data)] { /* uses of data */ };
// C++11 emulation
// of init capture

std::vector<double> data;
...
auto func =
  std::bind(
    [](const std::vector<double>& data) { /* uses of data */ },
    std::move(data)
);
  • [?]By default, the operator() member function inside the closure class generated from a lambda is const. That has the effect of rendering all data members in the closure const within the body of the lambda. The move-constructed copy of data inside the bind object is not const, however, so to prevent that copy of data from being modified inside the lambda, the lambda’s parameter is declared reference-to-const.
// C++11 emulation
// of init capture
// for mutable lambda std::move(data)

auto func =
std::bind(                               
[](std::vector<double>& data) mutable
{ /* uses of data */ },
);
  • It’s not possible to move-construct an object into a C++11 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.

Item 34: Prefer lambdas to std::bind.

[My Conclusions]

  • std::bind: parameter evaluation在bind调用时做,而非binded functor被调用时做
  • std::bind requires specifying the overloaded function.
  • lambda更易inline

  • 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.

The Concurrency API

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

  • Hardware threads are the threads that actually perform computation. Contemporary machine architectures offer one or more hardware threads per CPU core.
  • Software threads (also known as OS threads or system threads) are the threads that the operating system manages across all processes and schedules for execution on hardware threads.
  • std::threads are objects in a C++ process that act as handles to underlying software threads.

// TODO: oversubscription? context switch causes cache polluting?


Things to Remember

  • • 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.

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

  • • The std::launch::async launch policy means that f must be run asynchronously, i.e., on a different thread.
  • • The std::launch::deferred launch policy means that f may run only when get or wait is called on the future returned by std::async.2 That is, f’s execution is deferred until such a call is made. When get or wait is invoked, f will execute synchronously, i.e., the caller will block until f finishes running. If neither get nor wait is called, f will never run.
template<typename F, typename... Ts>
inline
std::future<typename std::result_of<F(Ts...)>::type> reallyAsync(F&& f, Ts&&... params)
{
    // return future
    // for asynchronous
    // call to f(params...)
    return std::async(std::launch::async,
                      std::forward<F>(f),
                      std::forward<Ts>(params)...);
}

  • • 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.

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

Unjoinable std::thread objects include:

  • Default-constructed std::threads. Such std::threads have no function to execute, hence don’t correspond to an underlying thread of execution.
  • std::thread objects that have been moved from. The result of a move is that the underlying thread of execution a std::thread used to correspond to (if any) now corresponds to a different std::thread.
  • std::threads that have been joined. After a join, the std::thread object no longer corresponds to the underlying thread of execution that has finished running.
  • std::threads that have been detached. A detach severs the connection between a std::thread object and the underlying thread of execution it corresponds to.

// TODO: destruction of a joinable thread causes program termination, why?

class ThreadRAII {
    public:
        enum class DtorAction { join, detach };

        ThreadRAII(std::thread&& t, DtorAction a)
            : action(a), t(std::move(t)) {}

        ~ThreadRAII()
        {
            if (t.joinable()) {
                if (action == DtorAction::join) {
                    t.join();
                } else {
                    t.detach();
                }
            }
        }

        ThreadRAII(ThreadRAII&&) = default;
        ThreadRAII& operator=(ThreadRAII&&) = default;

        std::thread& get() { return t; }

    private:
        DtorAction action;
        std::thread t;
};

  • 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.

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

// TODO

  • The destructor for the last future referring to a shared state for a nondeferred task launched via std::async blocks until the task completes. In essence, the destructor for such a future does an implicit join on the thread on which the asynchronously executing task is running.
  • The destructor for all other futures simply destroys the future object. For asynchronously running tasks, this is akin to an implicit detach on the underlying thread. For deferred tasks for which this is the final future, it means that the deferred task will never run.

Things to Remember

  • 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.

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

condition variable (condvar)

// detecting task

std::condition_variable cv; // condvar for event
std::mutex m;               // mutex for use with cv// detect event
cv.notify_one();            // tell reacting task
// wrong reacting task// prepare to react
{                                       // open critical section
    std::unique_lock<std::mutex> lk(m); // lock mutex
    cv.wait(lk);                        // wait for notify; this isn't correct!// react to event (m is locked)
}                                       // close crit. section; unlock m via lk's dtor// continue reacting (m now unlocked)
  • If the detecting task notifies the condvar before the reacting task waits, the reacting task will hang.
  • The wait statement fails to account for spurious wakeups. spurious wakeups: threading APIs (in many languages—not just C++) is that code waiting on a condition variable may be awakened even if the condvar wasn’t notified.
// reacting task// prepare to react
{                                       // open critical section
    std::unique_lock<std::mutex> lk(m); // lock mutex
    cv.wait(lk,
            []{ return whether the event has occurred; });
    …                                   // react to event (m is locked)
}                                       // close crit. section; unlock m via lk's dtor// continue reacting (m now unlocked)

shared boolean flag

// detecting task

std::atomic<bool> flag(false);  // shared flag// detect event
flag = true;                    // tell reacting task
// reacting task// prepare to react
while (!flag);      // wait for event// react to event

the cost of polling in the reacting task: during the time the task is waiting for the flag to be set, the task is essentially blocked, yet it’s still running. As such, it occupies a hardware thread that another task might be able to make use of, it incurs the cost of a context switch each time it starts or completes its time-slice, and it could keep a core running that might otherwise be shut down to save power.

combine the condvar and flag-based designs

// detecting task

std::condition_variable cv;
std::mutex m;
bool flag(false);               // not std::atomic// detect event
{
    std::lock_guard<std::mutex> g(m); // lock m via g's ctor
    flag = true;                      // tell reacting task (part 1)
}                               // unlock m via g's dtor
cv.notify_one();                // tell reacting task (part 2)
// reacting task// prepare to react
{                                   // as before
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, [] { return flag; });   // use lambda to avoid spurious wakeups// react to event (m is locked)
}
…                                   // continue reacting (m now unlocked)

void future

// One reacting task

std::promise<void> p;
void react();      // func for reacting task
void detect()      // func for detecting task
{
    std::thread t([] // create thread
        {
            p.get_future().wait();  // suspend t until
            react();                // future is set
        });
    …               // here, t is suspended prior to call to react
    p.set_value();  // unsuspend t (and thus call react)// do additional work
    t.join();       // make t unjoinable
}
  • Between a std::promise and a future is a shared state, and shared states are typically dynamically allocated. You should therefore assume that this design incurs the cost of heap-based allocation and deallocation.
  • a std::promise may be set only once. The communications channel between a std::promise and a future is a one-shot mechanism: it can’t be used repeatedly.
// Multiple reacting tasks

std::promise<void> p;
void detect()   // now for multiple reacting tasks
{
    auto sf = p.get_future().share();   // sf's type is `std::shared_future<void>`
    std::vector<std::thread> vt;        // container for reacting threads
    for (int i = 0; i < threadsToRun; ++i) {
        vt.emplace_back([sf]{
                sf.wait();      // wait on local copy of sf
                react(); });
    }
    … // detect hangs if this "…" code throws!
    p.set_value();              // unsuspend all threadsfor (auto& t : vt) {        // make all threads unjoinable
        t.join();
    }
}

Things to Remember

  • 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.

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

std::atomic<bool> valAvailable(false);
auto imptValue = computeImportantValue(); // compute value
valAvailable = true; // tell other task it's available
  • volatile: prevent compiler from optimizing redundant loads and dead stores
std::atomic<int> x;
auto y = x; // error!
y = x; // error!
  • Copy operations for std::atomic are deleted. Hardware generally can’t read x and write y in a single atomic operation.
  • std::atomic offers neither move construction nor move assignment.
std::atomic<int> y(x.load()); // read x
y.store(x.load()); // read x again

// compiler optimization
register = x.load(); // read x into register
std::atomic<int> y(register); // init y with register value
y.store(register); // store register value into y

Things to Remember

  • 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.

Tweaks

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

// Approach 1:
// overload for lvalues and rvalues
class Widget {
    public:
        void addName(const std::string& newName) { names.push_back(newName); }
        void addName(std::string&& newName)
        { names.push_back(std::move(newName)); }
    private:
        std::vector<std::string> names;
};

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

// Approach 3:
// pass by value
class Widget {
    public:
        void addName(std::string newName)
        { names.push_back(std::move(newName)); }
};
  1. You should only consider using pass by value. source code和object code只有一份函数实现。
  2. Consider pass by value only for copyable parameters. 对于move-only types(如std::unique_ptr),只需要提供rvalue版本。
  3. Pass by value is worth considering only for parameters that are cheap to move.
  4. You should consider pass by value only for parameters that are always copied.
class Widget {
    public:
        void addName(std::string newName)
        {
            if ((newName.length() >= minLen) &&
                    (newName.length() <= maxLen))
            {
                names.push_back(std::move(newName));
            }
        }
    private:
        std::vector<std::string> names;
};

Item 42: Consider emplacement instead of insertion.

  • emplace_back uses perfect forwarding
  • **Insertion functions take objects to be inserted, while emplacement functions take constructor arguments for objects to be inserted. **This difference permits emplacement functions to avoid the creation and destruction of temporary objects that insertion functions can necessitate.
void killWidget(Widget* pWidget);

std::list<std::shared_ptr<Widget>> ptrs;
ptrs.push_back(std::shared_ptr<Widget>(new Widget, killWidget));
ptrs.push_back({ new Widget, killWidget });
// leaks when exception
ptrs.emplace_back(new Widget, killWidget);

A second noteworthy aspect of emplacement functions is their interaction with explicit constructors.

// copy initialization
std::regex r1 = nullptr; // error! won't compile
// direct initialization
std::regex r2(nullptr); // compiles

Emplacement functions use direct initialization, which means they may use explicit constructors. Insertion functions employ copy initialization, so they can’t.

regexes.emplace_back(nullptr); // compiles. Direct init permits
                               // use of explicit std::regex
                               // ctor taking a pointer
regexes.push_back(nullptr);    // error! copy init forbids
                               // use of that ctor
⚠️ **GitHub.com Fallback** ⚠️