HFTrader/modern-cpp-cheatsheet
Cheatsheet for best practices of Modern C++ (taken from Effective Modern C++)
README
Effective Modern C++ Cheatsheet
- ref(s): reference(s)
- l-ref(s): lvalue reference(s)
- r-ref(s): rvalue reference(s)
- u-ref(s): universal reference(s)
- op(s): operation(s)
- expr(s): expression(s)
- var(s): variable(s)
- fn(s): function(s)
- init: initialize
- declr: declaration
- objs: object(s)
- ptr(s): pointer(s)
- ctor: constructor
- dtor: destructor
- fwd: forward
Chapter 1. Deducing Types
Item 1: Understand template type deduction
- Deduced type of T doesn't always match that of the parameter (i.e ParamType) in template fns
- With l-ref/r-ref args, compiler ignores reference-ness of an arg when deducing the type of T
- With u-refs, type deduction always distinguishes between l-values and r-values args
- With pass-by-value, ref-ness,
const and volatile are ignored if present in ParamType
- args that are raw array or fn types always decay to ptr types unless they initialize references
Item 2: Understand auto type deduction
auto plays the role of T while its type specifier (i.e including const or ref) as ParamType
auto always deduces std::initializer_list<T> for braced initializer e.g {1, 2, 3}
auto as function or lambda return type uses template type deduction, not auto type deduction
Item 3: Understand decltype
decltype on vars/exprs yields correct type mostly; it yields l-value ref for l-values exprs only
decltype(auto) includes ref-ness when deducing return type of a fn or lambda from definition
Item 4: Know how to view deduced types
- Update your code so that it leads to a compilation failure and you will see the type in diagnostics
std::type_info::name depends upon compiler implementation; use Boost.TypeIndex library
Item 5: Prefer auto declarations
auto prevents verbose declarations, uninitialized vars, and directly holds closures (lambdas)
- Use
auto esp. in place of std::vector<T>::size_type and std::unordered_map<T>::key_type
Item 6: How to fix undesired auto type deduction
- Use
auto with static_cast (a.k.a explicitly typed initializer idiom) to get correct types
- Never use
auto directly with invisible proxy classes such as std::vector<bool>::reference
Chapter 3. Moving to Modern C++
Item 7: Distinguish between () and {} when creating objects
- Braced (a.k.a uniform) initializer helps to prevent narrowing conversions and most vexing parse
- For overload-resolution, compiler always prefers
std::initializer_list for braced initializer
Item 8: Prefer nullptr to 0 and NULL
- Don't use
0 or NULL, use nullptr of type nullptr_t which represents pointers of all types!
Item 9: Prefer alias declarations to typedefs
- Alias declarations use
using keyword and support templatization while typedefs don't
- Alias declarations avoid 1)
::type suffix 2) typename prefix when referring to other typedefs
Item 10: Prefer scoped enums to unscoped enums
- Use
enum class instead of enum to limit scope of an enum members to just inside the enum
enum class uses int type by default, prevents implicit convs and allows fwd declarations
Item 11: Prefer deleted functions to private undefined ones
- Make unwanted functions (such as copy-ctors for move-only types)
public as well as delete
Item 12: Always declare overriding functions override
- Declare overriding fns in derived types
override; use final to prevent further inheritance
Item 13: Always prefer const_iterators to iterators
- Prefer
const_iterators to iterators for all STL containers e.g cbegin instead of begin
- For max generic code, don't assume the existence of member
cbegin; use std::begin instead
Item 14: Declare functions noexcept if they won't emit exceptions
- Declare fns
noexcept when they don't emit exceptions e.g fns that use wide contracts
- Always use
noexcept for move-operations, swap functions and memory allocation/deallocation
- When a
noexcept fn emits an exception: stack is possibly wound and program is terminated
Item 15: Use constexpr whenever possible
constexpr objs are const that are known at compile time; not all consts are constexpr tho
constexpr functions will produce results at compile time if their args are known during compile
- Declaring objs and fns
constexpr allows you to use your types at compile as well as runtime
Item 16: Make const member functions thread-safe
- Member fns that do not modify members of a type should be made
const and then thread-safe
- For synchronization, consider
std::atomic first and then move to std::mutex when required
Item 17: Understand special member function generation
- Default ctor is generated if no other ctor declared; most generated fns are
public/inline
- Declaring dtor and/or copy ops disables generation of default move ops and vice versa
- Copy assignment operator is generated if: 1) not already declared 2) no move op is declared
Chapter 4. Smart Pointers
Item 18: Use std::unique_ptr for exclusive-ownership of resource management
std::unique_ptr owns what it points to, is fast as raw ptr (*) and supports custom deleters
- Always return
std::unique_ptr from factory fns; converting it to a std::shared_ptr is easy
std::array, std::vector and std::string are generally better choices than raw arrays []
Item 19: Use std::shared_ptr for shared-ownership resource management
std::shared_ptr points to an object with shared ownership but doesn't own the object
std::shared_ptr stores/updates metadata on heap and can be 2x slower than std::unique_ptr
- Unless you want custom deleters, prefer
std::make_shared<T> for creating shared pointers
- Don't create multiple
std::shared_ptrs from a single raw ptr; it leads to undefined behavior
- For a
std::shared_ptr to this, inherit your class type from std::enable_shared_from_this
Item 20: Use std::weak_ptr for std::shared_ptr-like ptrs that can dangle
std::weak_ptr operates with the possibility that the obj it points to might have been destroyed
std::weak_ptr::lock() returns nullptr for destroyed objs, else std::shared_ptr always
std::weak_ptr can be used for caching, observer lists and for prevention of shared ptrs cycles
Item 21: Prefer std::make_unique and std::make_shared to direct use of new
make functions remove src code duplication, improve exception safety and are faster (sometimes)
make are bad for custom deleters, large mem objs and std::weak_ptrs that outlive shared ptrs
- Do not mix overloading, braced initializer and
std::initializer_list; use new if you do
- When using
new (in any case), prevent mem leaks by immediately passing it to a smart ptr always
Item 22: When using Pimpl idiom, define special mem fns in an implementation file
- Pimpl idiom puts members of a class type inside an impl (
struct Impl) and stores a ptr to it
- For
std::unique_ptr<Impl>, always implement your copy, move and dtor ops in an impl file
- With
std::shared_ptr<Impl>, no need to do that; shared ptr is roughly just as efficient here
Chapter 5. Rvalue references, move semantics and perfect forwarding
- Move semantics usually replace expensive copy ops (e.g copy ctor) with cheaper move ops
- Perfect forwarding forwards a fn's args to other fns params while strictly preserving types
Item 23: Understand std::move and std::forward
std::move performs an unconditional cast to an rvalue; you can then perform move ops
std::forward casts its input arg to an rvalue only if the arg is bound to an rvalue name
Item 24: Distinguish universal refs (u-refs) from rvalue refs
- U-refs (occur in
T&& and auto&&) cast lvalues to lvalue refs and rvalues to rvalue refs
- For refs to be universal, type deduction and non-
constness of the param type is a pre-req
Item 25: Understand when to use std::move and std::forward
- Universal references are usually a better choice than overloading fns for lvalues and rvalues
- Apply
std::move on rvalue refs and std::forward on universal-refs last time each is used
- Similarly, also apply
std::move or std::forward accordingly when returning by value from fns
- Never return local objs in fns using
std::move; it can prevent return value optimization (RVO)
Item 26: Avoid overloading on universal references
- Universal-refs should be used when client's code could pass either lvalue refs or rvalue refs
- Fns overloaded on u-refs usually get called more often than expected so you should avoid them
- Avoid perf-fwding ctrs; they are usually better matches for non-
const values than copy/move ops
Item 27: Alternatives to overloading universal-references
- Ref-to-const works but is less efficient while pass-by-value works but only for copyable types
- Tag dispatching (e.g using
std::true_type) takes u-refs and a second arg to help in matching
- Templates using
std::enable_if_t and std::decay_t also work for u-refs and they reads nicely
- Generally, u-refs have efficieny advantages but they sometimes suffer from usability disadvantages
Item 28: Understand reference collapsing
- Reference collapsing converts
& && to & (i.e lvalue ref) and && && to && (i.e rvalue ref)
- Reference collapsing occurs in
template and auto type deductions, alias declrs and decltype
Item 29: Assume that move ops are not present, no cheap, and not used
- Generally, moving objs is usually much cheaper then copying them e.g heap-based STL containers
- For some types e.g
std::array and std::string (with SSO) copying them can be just as efficient
Item 30: Perfect forwarding failure cases
- Perf-forwarding fails when template type deduction fails or deduces wrong type for the arg passed
- Careful with braced initializers, 0 or NULL for
nullptr and declr only integral const static members
- Similarly,
template and overloaded fn names; altho you can use static_cast to resolve fn overloads
- Finally, passing bit-field to perf-fwding fns is problematic because refs to bitfields don't exist
Chapter 6. Lambda Expressions
Item 31: Avoid default capture modes
- Avoid default
& or = captures for lambdas cause they can easily lead to dangling refs
- Fail cases:
& when they outlive the objects captured, = for mem types when they outlive this
- Compiler captures
static types by ref even though default capture-mode could be by-value
Item 32: Use init-capture to move objects into (lambda) closures
- Often called generalized lambda captures allow you to init objs inside a lambda capture expr
Item 33: Use decltype on auto&& params for std::forward
- Use
auto&& deduction in lambda's params for using std::forward to fwd to other functions
Item 34: Prefer lambdas to std::bind
- No convincing use-use for
std::bind after init capture based lambdas were introduced
Chapter 7. Concurrency API
Item 35: Prefer task-based programming to thread-based
std::thread acts as handle to an OS thread so you need to manage scheduling and oversubscription
- Use
std::async (aka task) with default launch policy to handle most of the corner cases for you
Item 36: Specify std::launch::async for truly asynchronous tasks
std::async's default policy can make it run either async (new thread) or sync (i.e upon .get())
- For
std::future_status::deferred on .wait_for(), you need to always call .get()
Item 37: Always make std::threads unjoinable on all paths
- You need to either call
.join() or .detach() on an std::thread before it destructs
- In dtor, calling
.join() leads to performance anomalies while .detach() undefined behavior
Item 38: Be aware of varying destructor behavior of thread handle
std::future blocks in dtor if policy is std::launch::async by calling an implicit join
std::shared_future blocks when, additionally, the given shared future is the last copy
std::packaged_task doesn't need a dtor policy; underlying std::thread (running it) does
Item 39: Consider void std::futures for one-shot communication (comm.)
- For simple comm.,
std::condition_variable, std::mutex and std::lock_guard is an overkill
- Use
std::future<void> and std::promise for one-time communication between two threads
Item 40: Use std::atomic for concurrency and volatile for special memory
std::atomic makes R/W thread-safe and prevents the reordering of R/Ws on atomic types
volatile tells compiler it's special memory and compiler doesn't optimize redundant R/Ws
std::atomic doesn't support copy or move ops but you can use .load() and .store() fns.
- Use
volatile for special memory and std::atomic for managing access to shared memory
Item 41: Consider pass-by-value for params which are always copied and cheap to move
- Consider pass-by-value for always-copied parameters instead of pass by
l/r/u-refs in fns.
- Prefer
r-refs parameters for move-only types to limit copying to exactly one move operation
- Do not pass-by-value for base class parameter types cause it will lead to the slicing problem
Item 42: Choose emplacement instead of insertion
- Use
.emplace versions instead of .push/.insert to avoid temp when adding to STL containers
- When value being added uses assignment,
.push/.insert work just as well as .emplace versions
- In a cont. of resource-managing types e.g unique_ptr,
.push/.insert can prevent corner cases
- Whn using
.emplace functions, be careful with args cause they can invoke explicit ctors