Need help with modern-cpp-cheatsheet?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

muqsitnawaz
165 Stars 11 Forks MIT License 4 Commits 1 Opened issues

Description

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

Services available

!
?

Need anything else?

Contributors list

No Data

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,
    lambda
    s,
    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
    template
    s, 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::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::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 v{10, 20}
    creates a vector with 10 and 20, not 10
    int
    s 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 class
    es 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
    for creating shared pointers
  • Don't create multiple
    std::shared_ptr
    s 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_ptr
    s outlive their
    std::shared_ptr
    s 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
    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::thread
    s, 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::thread
s 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::future
s 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
    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

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.