Skip to content

muqsitnawaz/modern-cpp-cheatsheet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 

Repository files navigation

Effective Modern C++ Cheatsheet

Shorthands

  1. ref(s): reference(s)
  2. op(s): operation(s)

Terms

  1. lvalue: typically an expression whose address can be taken e.g a variable name (auto x = 10;)
  2. rvalue: an expression whose address cannot be taken in C++ i.e before C++11 e.g literal types (10)
  3. lvalue-ref(erence): reference to an lvalue type typically denoted by & e.g auto& lvalue_ref = x;
  4. rvalue-ref(erence): reference to an rvalue type typically denoted by && e.g auto&& rvalue_ref = 10;
  5. copy-operations: copy-construct from lvalues using copy-constructor and copy-assignment operator
  6. move-operations move-construct from rvalues using move-constructor and move-assignment operator
  7. arguments: expressions passed to a function call at call site (could be either lvalues or rvalues)
  8. parameters: lvalue names initialized by arguments passed to a function e.g x in void foo(int x);
  9. callable objects: objects supporting member operator() e.g functions, lambdas, std::function etc
  10. declarations: introduce names and types without details e.g class Widget;, void foo(int x);
  11. definitions: provide implementation details e.g class Widget { ... };, void foo(int x) { ... }

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 functions
  • For lvalue-refs/rvalue-refs, compiler ignores the reference-ness of an arg when deducing type of T
  • With universal-refs, type deduction always distinguishes between l-value and r-value argument types
  • With pass-by-value, reference-ness, const and volatile are ignored if present in the ParamType
  • Raw arrays [] and function types always decay to pointer 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 and/or ref) as ParamType
  • For a braced initializer e.g {1, 2, 3}, auto always deduces std::initializer_list as its type
  • Corner case: auto as a callable return type uses template type deduction, not auto type deduction

Item 3: Understand decltype

  • decltype, typically used in function templates, determines a variable or an expression's type
  • decltype(auto), unlike auto, includes ref-ness when used in the return type of a callable
  • Corner case: decltype on lvalue expression (except lvalue-names) yields lvalue-refs not lvalues

Item 4: How to view deduced types?

  • You can update your code so that it leads to a compilation failure, you will see the type in diagnostics
  • std::type_info::name (and typeid()) depends upon compiler; use Boost.TypeIndex library instead

Chapter 2. auto

Item 5: Prefer auto declarations

  • auto prevents uninitialized variables and verbose declarations (e.g std::unordered_map<T>::key_type)
  • Use auto especially when declaring lambdas to directly hold closures unlike std::function

Item 6: How to fix undesired auto type deduction?

  • Use auto with static_cast (a.k.a explicitly typed initializer idiom) to enforce 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 {} (aka braced/uniform initializer) when creating objects

  • Braced initializer i.e {} prevents narrowing conversions and most vexing parse while () doesn't
  • During overload-resolution, std::initializer_list version is always preferred for {} types
  • Corner case: std::vector<int> v{10, 20} creates a vector with 10 and 20, not 10 ints initialized to 20.

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 (declared with using keyword) 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 classes use int by default, prevent implicit conversions and permit forward declarations

Item 11: Prefer public-deleted functions to private-undefined versions

  • Always make unwanted functions (such as copy-operations for move-only types) public and delete

Item 12: Always declare overriding functions override

  • Declare overriding functions 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 functions noexcept when they don't emit exceptions such as functions with wide contracts
  • Always use noexcept for move-operations, swap functions and memory allocation/deallocation
  • When a noexcept function emits an exception: stack is possibly wound and program is terminated

Item 15: Use constexpr whenever possible

  • constexpr objects are always const and usable in compile-time evaluations e.g template parameters
  • constexpr functions produce results at compile-time only if all of their args are known at compile-time
  • constexpr objects and functions can be used in a wider context i.e compile-time as well as runtime

Item 16: Make const member functions thread-safe

  • Make member functions of a type const as well as thread-safe if they do not modify its members
  • For synchronization issues, consider std::atomic first and then move to std::mutex if required

Item 17: Understand when your compiler generates special member functions

  • Compiler generates a default constructor only if the class type declares no constructors at all
  • Declaring destructor and/or copy ops disables the 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 pointer (*) and supports custom deleters
  • Conversion to a std::shared_ptr is easy, therefore factory functions should always return std::unique_ptr
  • std::array, std::vector and std::string are generally better choices than using 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 actually own the object
  • std::shared_ptr stores/updates metadata on heap and can be up to 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 pointer; it leads to undefined behavior
  • For std::shared_ptr to this, always inherit your class type from std::enable_shared_from_this

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

  • std::weak_ptr operates with the possibility that the object it points to might have been destroyed
  • std::weak_ptr::lock() returns a std::shared_ptr, but a nullptr for destroyed objects only
  • std::weak_ptr is typically used for caching, observer lists and prevention of shared pointers cycles

Item 21: Prefer make functions (i.e std::make_unique and std::make_shared) to direct use of new

  • Use make functions to remove source code duplication, improve exception safety and performance
  • When using new (in cases below), prevent memory leaks by immediately passing it to a smart pointer!
  • You must use new when 1) specifying custom deleters 2) pointed-to object is a braced initializer
  • Use new when std::weak_ptrs outlive their std::shared_ptrs to avoid memory de-allocation delays

Item 22: When using Pimpl idiom, define special member functions in an implementation file

  • Pimpl idiom puts members of a type inside an impl type (struct Impl) and stores a pointer to it
  • Use std::unique_ptr<Impl> and always implement your destructor and copy/move ops in an impl file

Chapter 5. Rvalue references, move semantics and perfect forwarding

  • Move semantics aim to replace expensive copy ops with the cheaper move ops when applicable
  • Perfect forwarding forwards a function's args to other functions parameters while preserving types

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

  • std::move performs an unconditional cast on lvalues to rvalues; 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 from rvalue-refs

  • Universal-refs (i.e T&& and auto&&) always cast lvalues to lvalue-refs and rvalues to rvalue-refs
  • For universal-ref parameters, auto/template type deduction must occur and they must be non-const

Item 25: Understand when to use std::move and std::forward

  • Universal references are usually a better choice than overloading functions 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 functions
  • Never return local objects from functions with 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
  • Functions overloaded on universal-refs typically get called more often than expected - avoid them!
  • Avoid perf-forwarding constructors because they can hijack copy/move ops for non-const types

Item 27: Alternatives to overloading universal-references

  • Ref-to-const works but is less efficient while pass-by-value works but use only for copyable types
  • Tag dispatching uses an additional parameter type called tag (e.g std::is_integral) to aid in matching
  • Templates using std::enable_if_t and std::decay_t work well for universal-refs and they read nicely
  • Universal-refs offer efficiency advantages although 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 declarations and decltype

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

  • Generally, moving objects 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: Be aware of failure cases of perfect forwarding

  • Perf-forwarding fails when template type deduction fails or deduces wrong type for the arg passed
  • Fail cases: braced initializers and passing 0 or NULL (instead of nullptr) for null pointers
  • For integral static const data members, perfect-forwarding will fail if you're missing their definitions
  • For overloaded or template functions, avoid fail cases using static_cast to your desired type
  • Don't pass bitfields directly to perfect-forwarding functions; use static_cast to an lvalue first

Chapter 6. Lambda Expressions

Item 31: Avoid default capture modes

  • Avoid default & or = captures for lambdas because they can easily lead to dangling references
  • Fail cases: & when they outlive the objects captured, = for member types when they outlive this
  • static types are always captured by-reference even though default capture mode could be by-value

Item 32: Use init-capture (aka generalized lambda captures) to move objects into (lambda) closures

  • Init-capture allows you to initialize types (e.g variables) inside a lambda capture expression

Item 33: Use decltype on auto&& parameters for std::forward

  • Use decltype on auto&& parameters when using std::forward for forwarding them to other functions
  • This case will typically occur when you are implementing perfect-forwarding using auto type deduction

Item 34: Prefer lambdas to std::bind

  • Always prefer init capture based lambdas (aka generalized lambdas) instead of using std::bind

Chapter 7. Concurrency API

Item 35: Prefer std::async (i.e task-based programming) to std::thread (i.e thread-based)

  • When using std::threads, you almost always need to handle scheduling and oversubscription issues
  • Using std::async (aka task) with default launch policy handles most of the corner cases for you

Item 36: Specify std::launch::async for truly asynchronous tasks

  • std::async's default launch policy can run either async (in new thread) or sync (upon .get() call)
  • If you get std::future_status::deferred on .wait_for(), call .get() to run the given task

Item 37: Always make std::threads unjoinable on all paths

  • Avoid program termination by calling .join() or .detach() on an std::thread before it destructs!
  • Calling .join() can lead to performance anomalies while .detach() leads to undefined behavior

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

  • std::future blocks in destructor 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 in scope
  • std::packaged_task doesn't need a destructor policy but the underlying std::thread (running it) does

Item 39: Consider std::futures of void type 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

  • Use std::atomic guarantees thread-safety for shared memory while volatile specifies special memory
  • std::atomic prevents reordering of reads/write operations but permits elimination of redundant reads/writes
  • volatile specifies special memory (e.g for memory mapped variables) which permits redundant reads/writes

Chapter 8. Tweaks

Item 41: When to use pass-by-value for functions parameters

  • Consider pass-by-value for parameters if and only if they are always copied and are cheap to move
  • Prefer rvalue-ref parameters for move-only types to limit copying to exactly one move operation
  • Never use pass-by-value for base class parameter types because it leads to the slicing problem

Item 42: Choose emplacement instead of insertion

  • Use .emplace versions instead of .push/.insert to avoid temp copies when adding to STL containers
  • When value being added uses assignment, .push/.insert work just as well as the .emplace versions
  • For containers of resource-managing types e.g smart pointers, .push/.insert can prevent memory leaks
  • Be careful when using .emplace functions because the args passed can invoke explicit constructors

Releases

No releases published

Packages

No packages published