C++

(nota: ctor = constructor, dtor = destructor)

nothrow new

In caso di errore nel ctor:

cv-qualifier

Ogni tipo è cv-qualified (const-qualified o volatile-qualified) ad eccezione dei function types e dei reference types.

Gli oggetti const non possono essere modificati; le modifiche a gli oggetti volatile sono considerate side effect ai fini dell'ottimizzazione.

Conversione tra tipi

C-style

(new_type)expr

Non viene fatto nessun controllo logico.

static_cast

static_cast<new_type>(expr)

Cast tra tipi correlati. new_type dev'essere cv-qualified almeno quanto old_type. Utile specie per void*.

reinterpret_cast

reinterpret_cast<new_type>(expr)

Cast tra tipi non correlati. Reinterpreta i bit in memoria. new_type dev'essere cv-qualified almeno quanto old_type.

Exception semantics

Man mano che l'eccezione bubbles up the call stack, viene fatto stack unwinding, cioè vengono chiamati i dtor degli oggetti con automatic storage duration.

Exception specifications

void f() throw (E1, E2); // May throw any of E1 or E2 void f() throw (); // No exceptions are thrown void f(); // Any exception may be thrown

Se vengono violate le specifications viene chiamato unexpected(); posso settare il valore di unexpected con set_unexpected().

Const

Const member functions

Garantisco di non modificare l'oggetto e di non chiamare membri non-const.

Const references

È l'equivalente di passare per valore, ma senza fare una copia.

Friend declarations

Una friend declaration dà accesso a un'altra funzione o classe ai membri private e protected.

class A { friend class B; friend void C(A&); }; class B {}; void C(A&) { // Can access private members of C }

Memory management

Copy ctor

Crea un oggetto come copia di un oggetto esistente. Spesso chiamato implicitamente dal compilatore quando un oggetto viene copiato.

Il copy ctor ha accesso a tutti i membri della classe, anche quelli privati.

Class(const Class &to_copy);

Move ctor

"Rubo" le risorse di un oggetto esistente -> diminuisco il consumo di memoria rispetto a una copia. Inoltre serve se ho una risorsa che non voglio condividere con una copia.

Class(const class &&to_move);

Se è necessario esplicitare l'uso del move ctor uso std::move:

T a(std::move(b)); T a = std::move(b); f(std::move(a)); return a; // ! Ritornare un valore lo passa per move

Move assignment

Class& operator=(Class&& to_move) noexcept;

Di norma o non implemento nè il move ctor nè il move assignment, o implemento entrambi

Rule of three

Se una classe richiede uno di questi tre, di norma li richiede tutti e tre:

  1. User-defined dtor
  2. User-defined copy ctor
  3. User-defined copy assignment

Rule of five

Se una classe implementa i tre metodi sopra, di default le move operations sono definite come deleted. Se voglio le move semantics devo definirle esplicitamente.

RAII

Resource Acquisition is Initialization: il lifetime di una risorsa è anche il lifetime della variabile corrispondente. -> Non usare mai new/delete fuori da una classe RAII

Ogni risorsa è incapsulata in una classe che ha la sola responsabilità di gestire la risorsa, acquisirla e rilasciarla. Di norma le copy operations sono deleted e vanno definite delle custom move operations.

RAII+move semantics fanno in modo che ogni risorsa sia owned da uno e un solo oggetto C++ in qualsiasi istante, e per trasferire l'ownership devo fare il move dell'oggetto.

Smart pointer

unique_ptr<T>

std::unique_ptr<T> implementa RAII/ownership semantics per puntatori arbitrari: fa free() quando l'unique_ptr va out of scope.

Ottengo il raw pointer con .get(), e con .release() ottengo il puntatore liberando l'ownership.

shared_ptr<T>

Implementa l'ownership condivisa: più oggetti hanno lo stesso puntatore (reference counting, l'ulitmo a perdere l'ownership fa free).

Creati con std::make_shared<T>().

STL

std::optional<T>

Value that might or might not exist. Use value() to access the value, has_value() to check if it has a value.

std::pair<T, U>

Stores one object of type T and one of type U.

std::tuple<T1, T2, ...>

Stores N objects of types T1, T2, ... Accessed with std::get<i>().

Container

Oggetti che immagazzinano una collezione di altri oggetti.

Ce ne sono di diversi tipi; gli iteratori implementano un'interfaccia comune, usata a sua volta dagli algoritmi.

std::vector<T>

Array (-> allocazione contigua) che può crescere dinamicamente.

std::unordered_map<K, V>

Container associativo di coppie key-value. Chiavi uniche.

std::map<K, V>

std::unordered_map<K, V> ma con chiavi ordinate.

Thread safety

A grandi linee: le letture simultanee su uno stesso container vanno bene, come anche letture e scritture simultanee su container diversi, e modifica di elementi diversi sullo stesso container. Per il resto serve sincronizzazione.

Iterators

Pointer abstractions: generalizzano i metodi di accesso a container diversi.

Implementano .begin(), ++ e .end().

Esiste una gerarchia di iteratori in base alle funzioni che implementano.

Function objects

In C++ le funzioni non sono oggetti, per cui di base non posso passarle (solo puntatori a funzione, lambda o classi che implementano stateful function objects).

Lambda

[captures] (params) -> ret { body }

captures:

Con [=] catturo tutte le variabili by copy, con [=var] specifico quali catturare.

std::function<R(T1, T2)>

General-purpose wrapper.

Algoritmi

Multithreading

void foo(int a, int b); std::thread t1(foo, 123, 456); std::thread t2([] { foo(123, 456;) }); // Init from lambda

Con .join() aspetto che un thread finisca.

Mutex

Ha lock(), try_lock() e unlock().

Il recursive mutex permette a un thread di lockare più volte lo stesso thread senza bloccarsi.

std::shared_mutex ha lock(), try_lock() e unlock() per l'exclusive locking, e lock_shared(), try_lock_shared() e unlock_shared() per lo shared locking.

L'unlock può essere fatto solo dal thread che ha fatto lock.

std::unique_lock locka il proprio mutex nel ctor (quando acquisisco la risorsa) e lo unlocka nel dtor. Inoltre è movable per trasferire l'ownership del mutex. Ad esempio:

std::mutex m; int i = 0; std::thread t([&] { std::unique_lock l{m}; // Lock ++i; // Unlock });

Se ho N lock diversi, std::lock (e il RAII wrapper std::scoped_lock) usa un deadlock-avoiding algorithm per acquisirli.

std::mutex m1, m2, m3; void threadA() { std::scoped_lock l{m1, m2, m3}; }

Condition variables

Usa un mutex per sincronizzare diversi thread e svegliarli quando una condition variable potrebbe essere modificata. Metodi: wait(), notify_one(), notify_all().

std::mutex m; std::condition_variable cv; std::queue<int> taskQueue; void pushWork(int task) { unique_lock l{m}; taskQueue.push(task); } cv.notify_one(); } void workerThread() { unique_lock l{m}; while(true) { if (!taskQueue.empty()) { int task = taskQueue.front(); taskQueue.pop(); l.unlock(); // do something l.lock(); } cv.wait(l); } }

Atomic ops

std::atomic<T> implementa load(), store(), e se wrappa un numero anche fetch_add(T arg) e fetch_sub(T arg) (atomic add).

std::future<T>

Con get() accedo (in modo blocking) a valori che saranno scritti in futuro da un "provider".

std::promise<T>

Una promise immagazzina un valore da recuperare nel future con .get_future().

Appena ho il valore chiamo set_value() per passarlo al future, o set_exception() (in quel caso .get() sul future tira l'eccezione).

std::async

Alternativa a std::thread per eseguire funzioni in parallelo.

std::future<T> async(launch_policy, function, args...);

launch_policy:

bool is_prime(int x); int main () { std::future<bool> fut = std::async( std::launch::async, is_prime, 117 ); bool ret = fut.get(); }

std::packaged_task<function_type>

Wrappa un callable element e permette di ottenerne il risultato in modo async.

bool is_prime(int x); int main() { packaged_task<bool(int)> tsk(is_prime); future<int> fut = tsk.get_future(); is_prime(1979); int r_value = fut.get(); }

std::shared_future<T>

È come un future ma può essere copiato, e diversi future possono avere l'ownership di uno stesso shared state. Inoltre il valore nello shared state può essere ottenuto più volte.

Panoramica: task-based vs thread-based

Task-based:

Thread-based: