HFTrader/modern-cpp-cheatsheet

Cheatsheet for best practices of Modern C++ (taken from Effective Modern C++)

★ 2Forks 2GitHub ↗Compare

README

Effective Modern C++ Cheatsheet

Abbreviations

  1. ref(s): reference(s)
  2. l-ref(s): lvalue reference(s)
  3. r-ref(s): rvalue reference(s)
  4. u-ref(s): universal reference(s)
  5. op(s): operation(s)
  6. expr(s): expression(s)
  7. var(s): variable(s)
  8. fn(s): function(s)
  9. init: initialize
  10. declr: declaration
  11. objs: object(s)
  12. ptr(s): pointer(s)
  13. ctor: constructor
  14. dtor: destructor
  15. 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

Chapter 2. auto

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

Chapter 8. Tweaks

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

Contributors

muqsitnawaz

Issues