Xavier Lamorlette

Effective Modern C++

Notes de lecture sur le livre “Effective Modern C++” de Scott Meyers, fusionnées avec d'autres notes sur C++11.

Effective Modern C++ book

Sommaire :

Chapter 1 - Deducing Types

Item 1 - Understand template type deduction

Universal reference = forwarding reference: function parameter declared like a rvalue reference (T && param) that memorises the type of reference (lvalue or rvalue) passed:

For template type deduction, the reference-ness of arguments is ignored.

Item 3 - Understand decltype

decltype(expr) is the exact compile-time type of the expression.

int x = 3;
decltype(x) y = x;

const vector<int> vi;
using CIT = decltype(vi.begin());
CIT another_const_iterator;

If expr is a lvalue expression of type T and not a name, decltype(expr) is T &.

C++14 introduces decltype(auto), which is notably useful for templates writing since auto does not preserve all qualifiers.

Item 4 - Know how to view deduced types

At compilation time
const int a = 1;
auto x = a;

template <typename T> class TypeDeduction;
TypeDeduction<decltype(x)> xType;
This will generate a compilation error such as:
error: aggregate 'TypeDeduction<int> xType' has incomplete type
At runtime
#include <boost/type_index.hpp>
std::cout << boost::typeindex::type_id_with_cvr<decltype(x)>().pretty_name();

Chapter 2 - Auto

Item 5 - Prefer auto to explicit type declarations

I disagree with this one: I think usage of auto generally decreases readability (but not in all cases, notably for STL iterators).

Item 6 - Use the explicitly type initialiser idiom when auto deduces undesired types

Useless if you don't overuse auto.

Chapter 3 - Moving to Modern C++

Item 7 - Distinguish between () and {} when creating objects

Brace initialisation:
int a{0};
string s{"hello"};
string s2{s};  // copy construction

vector<string> vs{"alpha", "beta", "gamma"};
map<string, string> capitals {
  {"fr", "Paris"},
  {"uk", "London"}
};

double * pd = new double [3] {0.5, 1.2, 12.99};

class C {
    int a = 7;
    int x[4];
public:
    C():
      x{0, 1, 2, 3} {
    }
};

struct A {
    A(std::initializer_list<string> strs) {
        for (const string & s: strs) {
            […]
        }
    };
}

My choice:

Item 8 - Prefer nullptr to 0 and NULL

void f(int);     // #1
void f(char *);  // #2
f(0);            // C++03: which f is called?
f(nullptr);      // C++11: unambiguous, calls #2

Item 9 - Prefer alias declarations to typedefs

using = typedef
using Doublet = tuple<int, double>;
Alias template:
template <typename T>
using MyAllocList = std::list<T, MyAlloc<T>>;
MyAllocList<Widget> widgetList;
C++11 type traits have equivalents in C++14 with _t suffix (using alias templates). Example:
std::remove_const<T>::type  // C++11
std::remove_const_t<T>      // C++14

Item 10 - Prefer scoped enums to unscoped enums

Unscoped enum:
enum Color {RED, GREEN, BLUE};
Scoped enum = strongly typed enum
enum class Color {RED, GREEN, BLUE};
Color color = Color::GREEN;
if (color == Color::RED)
Scoped enums can be forward declared:
enum class Color;

Item 11: Prefer deleted functions to private undefined ones

struct A {
    A(const A &) = delete;
    A & operator=(const A &) = delete;
};

Item 12: Declare overriding functions override

Methods specifiers:

Reference-qualified member functions:

DataType & data() & {        // for lvalues instances
  return values;             // return a lvalue
}

DataType && data() && {      // for rvalues instances
  return std::move(values);  // return a rvalue
}

Item 13 - Prefer const_iterators to iterators

For containers, cbegin() and cend() produce const_iterators (C++14).

insert() and erase() use const_iterators.

Item 14 - Declare functions noexcept if they won't emit exceptions

Specifier to instruct to not generate code for exceptions handling:
int g() noexcept;

All memory de-allocation functions and destructors are implicitly noexcept.

noexcept allows compiler optimisations, notably for move, swap, memory de-allocations and destructors: not generating code for exception handling means smaller code and cheaper calls to the function.

Item 15 - Use constexpr whenever possible

constexpr means known during compilation (and, of course, const). Example:

constexpr auto arraySize = 10;

constexpr functions produce compile-time constants when called with compile-time constants.

Constructors and methods can be constexpr. Example:

constexpr Point(int xVal=0, int yVal=0) noexcept:
  x(xVal),
  y(yVal) {
}
constexpr int getX() const noexcept {
  return x;
}
In C++14, even:
constexpr void setX(int xVal) noexcept {
  x = xVal;
}

Item 17 - Understand special member function generation

Declare default constructor, destructor, copy constructor, copy assignment, move constructor and move assignment operator = default to make intentions clear, and to avoid move operations not being generated if defining one of those later.

Use standard value classes to rely on default move and copy operations. For resource management, use smart pointers (along with = default where needed).

Chapter 4 - Smart Pointers

Item 18 - Use std::unique_ptr for exclusive ownership resource management

Item 19 - Use std::shared_ptr for shared ownership resource management

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

Item 21 - Prefer std::make_unique and std::make_shared to direct use of new

Item 22 - When using the PImpl idiom, define member functions in the implementation file

Voir Design Patterns - PImpl.

Chapter 5 - Rvalue References, Move Semantics, and Perfect Forwarding

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

std::move: unconditional cast to rvalue; transforms any sort of reference in a rvalue reference.

std::move(a) "~" static_cast<A &&>(a)

swap uses std::move.

Move requests on const objects (notably if using a rvalue reference to a const object) result in copies.

A parameter is always a lvalue, even if its type is a rvalue reference, hence the need for std::forward.

std::forward<T>: conditional cast to rvalue, if its argument was initialised with a rvalue.

Item 24 - Distinguish universal references from rvalue references

Universal references (aka perfect forwarding references) arise when there is type deduction. They preserve the argument's value category (lvalue / rvalue) and its const / volatile modifiers.

template <typename T>
void f(T && param);

auto && x = y;

No const for universal references.

Item 25 - Use std::move on rvalue references, std::forward on universal references

Do it:

Don't apply std::move on local objects because of RVO: Return Value Optimisation: if a function returns a local variable, the copy is avoided by constructing it in the memory allocated for the function's return value.

Item 26 - Avoid overloading on universal references

Because universal references are much greedier than expected. Beware especially of perfect forwarding constructors.

Item 28 - Understand reference collapsing

In universal / forwarding references T &&, for lvalues, T resolves to lvalue reference, then collapsing rules apply.

Declaring a reference to reference is illegal.

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

SSO: Small String Optimisation: string < 15 characters are stored in a buffer within the std::string object.

Moving doesn't work for small strings and arrays (but can work for contained objects).

Some STL operations use only move operations declared noexcept, to offer strong exception safety guarantee.

Chapter 6 - Lambda Expressions

Vocabulary:

Item 31 - Avoid default capture modes

Item 32 - Use init capture to move objects into closures

Init capture (C++14): generalised lambda capture. Example:

auto f = [data = std::move(data)] {
  …
};

Item 33 - Use decltype on auto && parameters to std::forward them

Generic lambda (C++14): lambda that uses auto in its parameter specification. Example:

auto f = [](auto x) {
  …
};

To forward:

auto f = [](auto && x) {
  return g(std::forward<decltype(x)>(x));
};

For usage of auto && in generic programming see “Auto Type Deduction in Range-Based For Loops” by Petr Zemek.

Chapter 7 - The Concurrency API

Item 35 - Prefer task-based programming to thread-based

std::async deals automatically with software threads management (thread exhausting, oversubscription, load balancing) and exceptions.

int doAsyncWork();

auto futureResult = std::async(doAsyncWork);
…
futureResult.get()

Item 36 - Specify std::launch::async if asynchronicity is essential

Launch policies:

auto fut = std::async(std::launch::async, f);

Beware thread local variables: with std::async you don't know if it is run in a different thread.

Item 37 - Make std::threads unjoinable on all paths

A thread is joinable when it is or could be running. If a std::thread is destroyed while still joinable, the program execution is terminated. Use RAII to solve this issue.

Item 38 - Be aware of varying thread handle destruction behaviour

The result of a callee (typically a std::promise) is stored in a shared state before being get by the caller (typically via a std::future).

Item 39 - Consider void futures for one-shot event communication

std::promise<void> p;

// detecting task
p.set_value();

// reacting task
p.get_future().wait();

Item 40 - Use std::atomic for concurrency, volatile for special memory

volatile: special memory such as memory-mapped I/O. Tells the compiler to not perform optimisation on operations on this memory.

std::atomic is for data accessed from multiple threads without using mutexes.

Chapter 8 - Tweaks

Item 41 - Consider pass by value for copyable parameters that are cheap to move and always copied

void addName(std::string newName) {
  names.push_back(std::move(newName));
}

// instead of:
void addName(const std::string & newName) {
  names.push_back(newName);
}
void addName(std::string && newName) {
  names.push_back(std::move(newName));
}

// or:
template <typename T>
void addName(T && newName) {
  names.push_back(std::forward<T>(newName));
}

This costs only ane extra move.

Beware pass by value is subject to the slicing problem.

Item 42 - Consider emplacement instead of insertion

emplace_back (instead of push_back): construct directly in the container with the constructors parameters. Also emplace_front (instead of push_front) and emplace (instead of insert).

La dernière mise à jour de cette page date de septembre 2020.

Le contenu de ce site est, en tant qu'œuvre originale de l'esprit, protégé par le droit d'auteur.
Pour tout commentaire, vous pouvez m'écrire à xavier.lamorlette@gmail.com.