EMC Reading Notes - yszheda/wiki GitHub Wiki
- If expr’s type is a reference, ignore the reference part.
- Then pattern-match expr’s type against ParamType to determine T.
- 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.
passed by value
- 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.
- 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.
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 ofT&
.
// 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
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
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
)
-
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 takingstd::initializer_list
s.
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_list
s is so strong, it prevails even if the best-matchstd::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
andstd::make_shared
resolve the problem by internally using parentheses and by documenting this decision as part of their interfaces. (Intuitive interface — Part I)
- 0和
NULL
是int,会导致编译器选择错误的重载函数。
- • Prefer
nullptr
to 0 andNULL
. - • Avoid overloading on integral and pointer types.
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.
- 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
- Scoped enumerators are much more strongly typed.
- 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.
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.
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();
// 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.
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.
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 areconst
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.
- • 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.
- 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).
-
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.
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
norstd::forward
do anything at runtime.
- 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.
compiler optimization:
- https://en.wikipedia.org/wiki/Copy_elision
- http://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization
template<typename T>
void logAndAdd(T&& name)
{
logAndAddImpl(
std::forward<T>(name),
std::is_integral<typename std::remove_reference<T>::type>()
); }
std::enable_if
: SFINAE
// TODO
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.
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更快。
variadic templates
template<typename... Ts>
void fwd(Ts&&... params) // accept any arguments
{
f(std::forward<Ts>(params)...); // forward them to f
}
- 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.
- Captures apply only to non-static local variables (including parameters) visible in the scope where the lambda is created.
// 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.
[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.
- 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::thread
s are objects in a C++ process that act as handles to underlying software threads.
// TODO: oversubscription? context switch causes cache polluting?
- • 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.
- • 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.
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.
// 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.
- 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.
// 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)
// 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.
// 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)
// 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 afuture
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 threads
…
for (auto& t : vt) { // make all threads unjoinable
t.join();
}
}
- 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::promise
s and futures dodges these issues, but the approach uses heap memory for shared states, and it’s limited to one-shot communication.
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
-
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.
// 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)); }
};
- You should only consider using pass by value. source code和object code只有一份函数实现。
- Consider pass by value only for copyable parameters. 对于move-only types(如std::unique_ptr),只需要提供rvalue版本。
- Pass by value is worth considering only for parameters that are cheap to move.
- 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;
};
-
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