Salta al contenuto
Note per Studenti Esercizio - Quiz e domande su spostamento, passaggio dei parametri e contenitori STL (primo appello 2023-24, teoria)

Esercizio - Quiz e domande su spostamento, passaggio dei parametri e contenitori STL (primo appello 2023-24, teoria)

Esame
In questa pagina 4

Testo. Prova scritta di Laboratorio di Programmazione, primo appello 2023-24, parte di teoria (90 minuti): un quiz di 10 domande, ciascuna con 4 affermazioni da dichiarare vere o false (risposta corretta +1, sbagliata -0,25, non data 0; totale normalizzato a 10), e tre domande a risposta aperta (5, 4 e 4 punti).

Teoria: C++ - costruttori, inizializzazione, copia e spostamentouna buona interfaccia è completa e minimale, ha i costruttori giusti, supporta o proibisce la copia, usa tipi adeguati, dichiara const ciò che non modifica e libera le risorse nel distruttore; il costruttore di default (senza argomenti) esiste quando c'è un valore iniziale sensato, si ottiene con valori di default nei membri (in-class initializer) o argomenti di default, e explicit vieta la conversione implicita da un argomento; initializer_list permette la lista {a, b, c} e con le graffe ha la precedenza sul costruttore a un intero (vector{3} è un elemento, vector(3) sono tre); la copia di default è membro a membro (shallow copy) e con un puntatore a un buffer duplica il puntatore invece del contenuto (due oggetti che condividono i dati e una doppia delete), quindi servono il costruttore di copia e l'assegnamento di copia che fanno la deep copy (l'assegnamento prima alloca e copia, poi libera i vecchi dati, e deve tollerare l'auto-assegnamento); this è il puntatore all'oggetto corrente, parametro implicito delle funzioni membro; il costruttore e l'assegnamento di spostamento (T&&, rvalue reference) rubano il buffer della sorgente lasciandola vuota ma valida, il compilatore li sceglie da solo (con la copy elision a volte non servono nemmeno); i cinque (distruttore, copia, spostamento) vanno definiti insieme.C++ - costruttori, inizializzazione, copia e spostamento →, C++ - funzioni, reference e passaggio dei parametriuna funzione ha parametri formali (0 o più, input) e un valore di ritorno (0 o 1, output); in C++ gli argomenti si passano per valore, cioè il parametro è una variabile locale inizializzata con una copia dell'argomento, e modificarla non cambia l'originale; per oggetti grandi (vector, immagini) la copia costa, e si passa per reference (T&), un alias dell'oggetto del chiamante che permette lettura e scrittura, oppure per const reference (const T&), alias in sola lettura verificata dal compilatore, che accetta anche literal (oggetto temporaneo); regola: valore per oggetti piccoli, const T& per grandi da non modificare, T& solo se la funzione deve davvero modificare o produrre più di un output; il passaggio converte l'argomento nel tipo del parametro come in un'inizializzazione e un cast esplicito (static_cast) rende visibile l'intenzione.C++ - funzioni, reference e passaggio dei parametri →, STL - sequenze, iteratori e container (vector, list, string)La STL (Standard Template Library) fornisce contenitori (vector, list, map, ...) e algoritmi generici (sort, find, accumulate, ...) tenuti insieme da una interfaccia unificata: la sequenza, un intervallo [begin, end) di elementi gestito tramite iteratori (oggetti che identificano un elemento: * dereferenzia, ++ avanza, == e != confrontano; end è il primo elemento dopo l'ultimo, quindi una sequenza vuota ha begin == end). Gli algoritmi conoscono solo gli iteratori e i contenitori espongono solo iteratori, così i due mondi sono disaccoppiati e cambiare contenitore non cambia l'algoritmo. Un iteratore di lista è una classe annidata con un puntatore al nodo; auto deduce il tipo dell'iteratore. vector (contiguo, [] e at(), insert/erase costosi e che invalidano gli iteratori), string (un vector di char con + e +=), list (nodi concatenati, insert/erase senza spostare gli altri elementi, niente []) e char[] (array C: dimensione fissa, niente iteratori, == confronta i puntatori) a confronto.STL - sequenze, iteratori e container (vector, list, string) →. La parte di programmazione dello stesso appello è Esercizio - Rubrica di contatti con list e sort (primo appello 2023-24, programmazione).


Quiz

Per ogni affermazione: V o F e il perché.

1. In C++ il costruttore di copia di default di una classe:

affermazione risposta motivo
è disabilitato dalla presenza del costruttore di spostamento definito dal programmatore V dichiarando un costruttore (o assegnamento) di spostamento, il costruttore di copia implicito viene cancellato (= delete): chi vuole la copia deve scriverla
può essere sovrascritto dal programmatore V il programmatore può definire un proprio costruttore di copia, che sostituisce quello generato
effettua una deep copy dei membri allocati dinamicamente F copia membro per membro: un puntatore viene copiato come valore (shallow copy), i due oggetti condividono il blocco
produce un errore di compilazione se usato per oggetti con membri allocati dinamicamente F compila: l'errore è a run-time (doppia delete), non di compilazione

2. In C++ l'operatore =:

affermazione risposta motivo
per essere usato con un tipo definito dall'utente deve essere definito esplicitamente F il compilatore genera l'assegnamento di copia (membro per membro)
confronta due variabili F confronta ==; = assegna
chiama l'operatore di assegnamento di una classe V per un oggetto, a = b è a.operator=(b)
è un operatore con side effect V modifica l'operando di sinistra

3. La classe std::vector:

affermazione risposta motivo
[] può essere usato solo per l'accesso in lettura F restituisce un riferimento: si può anche scrivere (v[i] = x)
at() controlla la dimensione e lancia un'eccezione fuori dai limiti V out_of_range
ha un numero di elementi costante definito a tempo di compilazione F si ridimensiona a run-time (è std::array ad avere dimensione fissa)
usa l'allocazione dinamica per il suo funzionamento V il buffer è nel free store

4. Le include guard:

affermazione risposta motivo
servono a evitare le inclusioni multiple e/o ricorsive V
si rendono con #ifndef ... #define ... #endif, come da standard C++ V è il metodo standard (#pragma once non lo è)
servono a proteggere in scrittura una variabile F è compito di const
si usano solo con classi template F in ogni header

5. La compilazione di un progetto C++:

6. Un function object:

affermazione risposta motivo
è una classe che deve fare l'overloading di operator() V
è una classe che deve fare l'overloading di operator[] F
non è copiabile F si passa per copia agli algoritmi
può essere usato per esprimere un predicato V se operator() restituisce bool (STL - algoritmi, predicati e lambdaLa STL ha circa 80 algoritmi generici (header e ) che lavorano su sequenze [first, last) di qualunque contenitore tramite iteratori: find/find_if, count/count_if, sort, copy/copy_if/unique_copy, merge, equal_range, equal, accumulate, inner_product. find cerca un valore e restituisce l'iteratore alla prima occorrenza o end(); find_if verifica un predicato (funzione che restituisce bool) passato come puntatore a funzione, function object (classe con operator()) o lambda expression. Una variabile globale per passare la soglia al predicato è da evitare; il function object la memorizza in un dato membro. Una lambda cattura -> tipo { corpo } definisce la funzione sul posto e può catturare le variabili locali per valore ([=], [x]) o per riferimento ([&], [&x]). sort(b, e) ordina con < e sort(b, e, cmp) con un criterio, solo su iteratori random access; copy richiede spazio a destinazione.STL - algoritmi, predicati e lambda →)

7. Nello stack, i record di attivazione (RA) delle funzioni:

8. In C++ un'enumerazione:

affermazione risposta motivo
è equivalente a una classe con tutti i membri pubblici F è un insieme di costanti con nome
crea uno scope se è definita come enum class V si scrive Colore::Rosso
rende più efficiente l'esecuzione F la scelta è di espressività, non di velocità
può rendere il codice più espressivo V nomi al posto di numeri magici

9. Una classe astratta in C++:

10. Codice: class Foo { public: void print(void) { std::cout << value << std::endl; } private: int value = 10; }; con main che definisce Foo my_foo; my_foo.print();.

affermazione risposta motivo
il membro value di my_foo è contenuto nello stack V my_foo è una variabile locale automatica e i suoi dati membro sono dentro l'oggetto
l'eseguibile stampa 10 V inizializzatore del membro = 10
my_foo è un oggetto locale automatico V
la funzione print() ha scope globale F è una funzione membro: scope di classe

Domanda aperta 1 (5 punti): costruttore e assegnamento di spostamento

Testo. Spiegare che cosa sono il costruttore e l'assegnamento di spostamento e in quali casi sono usati dal compilatore. Riportare un esempio di classe per cui implementare le funzioni di spostamento migliora l'efficienza. Implementare, oltre alle due funzioni citate, anche il costruttore di default e il distruttore. Riportare un esempio di codice in cui è usato il costruttore di spostamento e uno in cui è usato l'assegnamento di spostamento (ignorare la copy elision).

Risposta.

Che cosa sono. Il costruttore di spostamento C(C&&) crea un oggetto "rubando" le risorse di un altro, invece di copiarle; l'assegnamento di spostamento C& operator=(C&&) fa lo stesso su un oggetto già esistente (dopo aver liberato le sue risorse). Il parametro è un riferimento rvalue (&&): si lega a un oggetto temporaneo o a uno che si dichiara "non più necessario" con std::move. L'oggetto sorgente resta valido ma svuotato, pronto per essere distrutto o riassegnato.

Quando le usa il compilatore. Quando l'origine è un rvalue: un temporaneo (Buffer b = make();, valore restituito da funzione, risultato di un'espressione), oppure un std::move(x) esplicito; i contenitori della libreria (come vector) spostano gli elementi quando riallocano, se lo spostamento è noexcept. Per un lvalue il compilatore sceglie la copia. Il compilatore genera lo spostamento da solo solo se la classe non dichiara copia, spostamento o distruttore (regola dei cinque).

Perché migliora l'efficienza. Per una classe che possiede una risorsa (un buffer dinamico di n elementi) la copia costa O(n)O(n) (alloca e copia), lo spostamento O(1)O(1) (copia due variabili e azzera la sorgente). Esempio con un contatore delle copie degli elementi:

a1t/move_buffer.cppcpp
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>

// Un array dinamico di int che conta quanti ELEMENTI vengono copiati: cosi' si vede
// la differenza di costo tra copia e spostamento.
class Buffer {
public:
    static long element_copies;                           // contatore globale di copie di elementi

    Buffer() : n_{0}, data_{nullptr} {}                   // costruttore di default
    explicit Buffer(std::size_t n) : n_{n}, data_{new int[n]{}} {}
    ~Buffer() { delete[] data_; }                         // distruttore

    Buffer(const Buffer& o) : n_{o.n_}, data_{new int[o.n_]}          // copia profonda: costa O(n)
    {
        std::copy(o.data_, o.data_ + n_, data_);
        element_copies += static_cast<long>(n_);
    }
    Buffer& operator=(const Buffer& o)
    {
        if (this != &o) {
            int* fresh = new int[o.n_];
            std::copy(o.data_, o.data_ + o.n_, fresh);
            element_copies += static_cast<long>(o.n_);
            delete[] data_;
            data_ = fresh;
            n_ = o.n_;
        }
        return *this;
    }

    Buffer(Buffer&& o) noexcept : n_{o.n_}, data_{o.data_}            // spostamento: "ruba" il buffer, costa O(1)
    {
        o.n_ = 0;
        o.data_ = nullptr;                                            // la sorgente resta valida e vuota
    }
    Buffer& operator=(Buffer&& o) noexcept
    {
        if (this != &o) {
            delete[] data_;                                           // libera il proprio buffer
            data_ = o.data_;                                          // prende quello della sorgente
            n_ = o.n_;
            o.data_ = nullptr;
            o.n_ = 0;
        }
        return *this;
    }

    std::size_t size() const { return n_; }

private:
    std::size_t n_;
    int* data_;
};
long Buffer::element_copies = 0;

Buffer make(std::size_t n) { Buffer b{n}; return b; }                 // restituzione per valore

int main()
{
    Buffer a{1000};
    std::cout << "costruiti 1000 elementi, copie finora: " << Buffer::element_copies << '\n';

    Buffer b = a;                                                     // costruttore di COPIA (a e' un lvalue)
    std::cout << "copia:      copie = " << Buffer::element_copies << ", a.size = " << a.size() << '\n';

    Buffer c = std::move(a);                                          // costruttore di SPOSTAMENTO
    std::cout << "spostamento: copie = " << Buffer::element_copies << ", a.size = " << a.size()
              << ", c.size = " << c.size() << '\n';

    Buffer d;
    d = std::move(b);                                                 // assegnamento di SPOSTAMENTO
    std::cout << "assegn. spost.: copie = " << Buffer::element_copies << ", b.size = " << b.size()
              << ", d.size = " << d.size() << '\n';

    Buffer e;
    e = c;                                                            // assegnamento di COPIA (c e' un lvalue)
    std::cout << "assegn. copia: copie = " << Buffer::element_copies << '\n';

    Buffer f = make(500);                                             // temporaneo: spostamento o elisione, nessuna copia
    std::cout << "da funzione: copie = " << Buffer::element_copies << ", f.size = " << f.size() << '\n';

    std::vector<Buffer> v;                                            // il vector, riallocando, sposta se e' noexcept
    long prima = Buffer::element_copies;
    for (int i = 0; i < 5; ++i) v.push_back(Buffer{100});            // rvalue: push_back(Buffer&&)
    std::cout << "5 push_back di temporanei: copie aggiunte = " << Buffer::element_copies - prima << '\n';
}

Uscita eseguita (compilato anche per Linux con -Wall -Wextra senza avvisi):

costruiti 1000 elementi, copie finora: 0
copia:      copie = 1000, a.size = 1000
spostamento: copie = 1000, a.size = 0, c.size = 1000
assegn. spost.: copie = 1000, b.size = 0, d.size = 1000
assegn. copia: copie = 2000
da funzione: copie = 2000, f.size = 500
5 push_back di temporanei: copie aggiunte = 0

Lettura: Buffer b = a; è una copia (1000 elementi copiati). Buffer c = std::move(a); usa il costruttore di spostamento (nessun elemento copiato, a resta vuoto); d = std::move(b); usa l'assegnamento di spostamento; e = c; è una copia perché c è un lvalue; make(500) e i push_back di temporanei non copiano niente.

Esempi richiesti. Costruttore di spostamento: Buffer c = std::move(a); (oppure Buffer c{make(500)} ignorando l'elisione). Assegnamento di spostamento: d = std::move(b); (oppure d = make(500);). Entrambi lasciano la sorgente con data_ == nullptr e n_ == 0.

Punti che conviene dichiarare nella risposta: gli spostamenti sono noexcept (serve ai contenitori per usarli); nello spostamento si controlla l'auto-assegnamento (this != &o) e si libera il proprio buffer prima di prendere quello dell'altro; la sorgente va lasciata in uno stato distruggibile.

Domanda aperta 2 (4 punti): il passaggio degli argomenti a una funzione

Testo. Descrivere i diversi modi con cui avviene il passaggio di argomenti a una funzione in C++. Per ognuno riportare i meccanismi coinvolti e i vantaggi/svantaggi. Corredare ogni modo con un esempio di codice.

Risposta.

modo firma meccanismo vantaggi svantaggi
per valore void f(int x) il parametro è una copia dell'argomento (costruttore di copia); la funzione lavora sulla copia l'originale è al sicuro; accetta anche temporanei e costanti la copia costa per oggetti grandi; non si possono restituire risultati nei parametri
per riferimento void f(int& x) il parametro è un alias dell'argomento: nessuna copia efficiente; permette di modificare l'originale (parametri di uscita); sintassi uguale al valore non accetta temporanei; dal chiamante non si vede che f può modificare l'argomento
per riferimento const void f(const T& x) alias in sola lettura efficiente e sicuro per i tipi grandi; accetta anche temporanei e costanti la funzione non può modificare l'argomento
per puntatore void f(int* p) si passa una copia dell'indirizzo; la funzione accede all'originale con *p permette di modificare l'originale; il puntatore può essere nullptr ("nessun argomento"); è il meccanismo degli array C sintassi più pesante (&x, *p); va controllato nullptr; rischio di dangling; la riassegnazione di p è locale

Esempio completo e verificato:

a1t/parametri.cppcpp
#include <iostream>
#include <string>
#include <vector>
using namespace std;

void per_valore(int x)            { x = 100; cout << "  nella funzione x = " << x << '\n'; }   // lavora su una COPIA
void per_riferimento(int& x)      { x = 100; }                 // lavora sull'originale
void per_const_ref(const int& x)  { cout << "  const&: leggo " << x << " senza copiare\n"; }
void per_puntatore(int* p)        { if (p) *p = 100; }         // lavora sull'originale, puo' essere nullptr
void per_ptr_const(const int* p)  { cout << "  const*: leggo " << *p << '\n'; }

void somma_grande(const vector<int>& v, int& totale)           // uso tipico: grande in ingresso, risultato in uscita
{
    totale = 0;
    for (int x : v) totale += x;
}

void riassegna_puntatore(int* p, int* altro)                   // il puntatore e' copiato: la riassegnazione e' locale
{
    p = altro;
    cout << "  nella funzione p punta ad altro: " << (p == altro) << '\n';
}

int main()
{
    int a = 1, b = 1, c = 1, d = 1;
    per_valore(a);
    per_riferimento(b);
    per_puntatore(&c);
    cout << "dopo le chiamate: a = " << a << " (valore), b = " << b << " (riferimento), c = " << c
         << " (puntatore)\n";

    per_const_ref(a);
    per_ptr_const(&b);
    per_puntatore(nullptr);                                    // lecito: il puntatore puo' non puntare a niente
    // per_riferimento(5);                                     // errore: un rvalue non si lega a int&
    per_const_ref(5);                                          // ok: const& si lega anche a un temporaneo

    vector<int> v{1, 2, 3, 4};
    int totale;
    somma_grande(v, totale);
    cout << "somma = " << totale << '\n';

    int *p = &c, *q = &d;
    riassegna_puntatore(p, q);                                 // p, nel main, e' ancora &c
    cout << "p punta ancora a c: " << (p == &c) << '\n';
}

Uscita eseguita:

  nella funzione x = 100
dopo le chiamate: a = 1 (valore), b = 100 (riferimento), c = 100 (puntatore)
  const&: leggo 1 senza copiare
  const*: leggo 100
  const&: leggo 5 senza copiare
somma = 10
  nella funzione p punta ad altro: 1
p punta ancora a c: 1

Il chiamante vede la variabile modificata solo con riferimento (non const) o puntatore (non a const); per valore no. Il puntatore stesso, però, è passato per valore: riassegnarlo dentro la funzione non cambia quello del chiamante (l'ultima riga). Regola d'uso: tipi piccoli (int, double) per valore; oggetti grandi in sola lettura per const&; risultati in uscita per riferimento o puntatore; nullptr come "opzionale" con il puntatore.

Domanda aperta 3 (4 punti): contenitori STL e accesso uniforme

Testo. Descrivere il concetto di contenitore STL. Descrivere il concetto di accesso uniforme ai dati nei contenitori STL e quale strumento viene utilizzato. Riportare due esempi di contenitori STL spiegando come sono strutturati.

Risposta.

Contenitore STL. Un contenitore STL è una sequenza di elementi [begin(), end()) che supporta la copia (costruttore di copia e assegnamento), chiama il tipo degli elementi value_type, ha i tipi iteratore iterator e const_iterator, e offre le operazioni comuni (insert, erase, front, back, push_back, pop_back, size, ...) e i confronti ==, !=, <, ... Esempi: vector, list, deque, map, set, array.

Accesso uniforme. Gli algoritmi (find, sort, accumulate, ...) e i contenitori sono disaccoppiati: l'algoritmo non conosce la struttura dati, il contenitore non conosce l'algoritmo; si parlano solo tramite iteratori. Uno iteratore è un oggetto che identifica un elemento della sequenza (o uno dopo l'ultimo) e supporta * (accesso), ++ (avanzamento), == e !=. Quindi lo stesso codice funziona con vector, list o un array C:

a1t/uniforme.cppcpp
#include <algorithm>
#include <iostream>
#include <list>
#include <string>
#include <vector>
using namespace std;

// Una sola funzione per qualsiasi contenitore: parla solo con gli iteratori [first, last)
template <typename Iterator>
int conta_maggiori(Iterator first, Iterator last, int soglia)
{
    int n = 0;
    for (Iterator p = first; p != last; ++p)
        if (*p > soglia) ++n;
    return n;
}

int main()
{
    vector<int> v{5, 12, 7, 20};
    list<int> l{3, 30, 8};
    int a[] = {50, 1, 60};

    cout << conta_maggiori(v.begin(), v.end(), 6) << ' '       // vector: 3
         << conta_maggiori(l.begin(), l.end(), 6) << ' '       // list: 2
         << conta_maggiori(a, a + 3, 6) << '\n';               // array C: 2

    // gli algoritmi STL usano la stessa interfaccia
    cout << (find(v.begin(), v.end(), 7) != v.end()) << (find(l.begin(), l.end(), 7) != l.end()) << '\n';  // 1 0

    // insert / erase: stessa notazione, costi diversi (vector sposta, list no)
    v.insert(v.begin() + 1, 99);
    l.insert(++l.begin(), 99);
    cout << v[1] << ' ' << *++l.begin() << '\n';
}

Uscita eseguita: 3 2 2, 10, 99 99. La funzione conta_maggiori lavora con tre tipi di sequenza; find si chiama allo stesso modo su vector e list; insert ha la stessa notazione ma costi diversi.

Due contenitori.

  • vector: un array dinamico: elementi contigui in un buffer nel free store, con size e capacity; accesso diretto con [] (e at() con controllo), push_back ammortizzato costante (si raddoppia la capacità riallocando); insert ed erase nel mezzo sono costosi (spostano gli elementi) e invalidano gli iteratori.
  • list: una lista doppiamente concatenata: ogni elemento sta in un nodo con puntatori al precedente e al successivo (Link {prev, succ, val}), più i puntatori al primo e all'ultimo; non c'è []; insert/erase in tempo costante senza spostare gli altri elementi, e gli iteratori degli altri elementi restano validi. Il suo iteratore è una classe annidata con un puntatore al nodo e gli operatori *, ++, --, ==, !=.

Versione ripasso

  • Quiz (risposte V/F): 1) disabilitato dal move: V; sovrascrivibile: V; deep copy: F; errore di compilazione: F. 2) = definito esplicitamente: F; confronta: F; chiama l'assegnamento: V; side effect: V. 3) [] solo lettura: F; at() controlla: V; elementi costanti a compile time: F; allocazione dinamica: V. 4) evitano inclusioni multiple: V; #ifndef standard: V; protezione in scrittura: F; solo template: F. 5) linker segnala sintassi: F; g++ segnala conversioni: V; g++ segnala leak: F; linking = parte mancante: V. 6) function object: operator() V; operator[] F; non copiabile F; predicato V. 7) RA con variabili dinamiche F; con locali automatiche V; dimensione fissa F; RA distinto a ogni chiamata ricorsiva V. 8) enum = classe pubblica F; enum class crea scope V; più efficiente F; più espressivo V. 9) astratta = almeno una virtuale pura V; niente dati F; solo con allocazione dinamica F; interfaccia comune V. 10) value nello stack V; stampa 10 V; my_foo locale automatico V; print() scope globale F.
  • Spostamento: C(C&&) e operator=(C&&) rubano le risorse (parametro rvalue, sorgente valida ma vuota, noexcept); usati per temporanei e std::move, non per lvalue; copia O(n)O(n), spostamento O(1)O(1). Costruttore: Buffer c = std::move(a);; assegnamento: d = std::move(b);.
a1t/move_buffer.cppcpp
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>

// Un array dinamico di int che conta quanti ELEMENTI vengono copiati: cosi' si vede
// la differenza di costo tra copia e spostamento.
class Buffer {
public:
    static long element_copies;                           // contatore globale di copie di elementi

    Buffer() : n_{0}, data_{nullptr} {}                   // costruttore di default
    explicit Buffer(std::size_t n) : n_{n}, data_{new int[n]{}} {}
    ~Buffer() { delete[] data_; }                         // distruttore

    Buffer(const Buffer& o) : n_{o.n_}, data_{new int[o.n_]}          // copia profonda: costa O(n)
    {
        std::copy(o.data_, o.data_ + n_, data_);
        element_copies += static_cast<long>(n_);
    }
    Buffer& operator=(const Buffer& o)
    {
        if (this != &o) {
            int* fresh = new int[o.n_];
            std::copy(o.data_, o.data_ + o.n_, fresh);
            element_copies += static_cast<long>(o.n_);
            delete[] data_;
            data_ = fresh;
            n_ = o.n_;
        }
        return *this;
    }

    Buffer(Buffer&& o) noexcept : n_{o.n_}, data_{o.data_}            // spostamento: "ruba" il buffer, costa O(1)
    {
        o.n_ = 0;
        o.data_ = nullptr;                                            // la sorgente resta valida e vuota
    }
    Buffer& operator=(Buffer&& o) noexcept
    {
        if (this != &o) {
            delete[] data_;                                           // libera il proprio buffer
            data_ = o.data_;                                          // prende quello della sorgente
            n_ = o.n_;
            o.data_ = nullptr;
            o.n_ = 0;
        }
        return *this;
    }

    std::size_t size() const { return n_; }

private:
    std::size_t n_;
    int* data_;
};
long Buffer::element_copies = 0;

Buffer make(std::size_t n) { Buffer b{n}; return b; }                 // restituzione per valore

int main()
{
    Buffer a{1000};
    std::cout << "costruiti 1000 elementi, copie finora: " << Buffer::element_copies << '\n';

    Buffer b = a;                                                     // costruttore di COPIA (a e' un lvalue)
    std::cout << "copia:      copie = " << Buffer::element_copies << ", a.size = " << a.size() << '\n';

    Buffer c = std::move(a);                                          // costruttore di SPOSTAMENTO
    std::cout << "spostamento: copie = " << Buffer::element_copies << ", a.size = " << a.size()
              << ", c.size = " << c.size() << '\n';

    Buffer d;
    d = std::move(b);                                                 // assegnamento di SPOSTAMENTO
    std::cout << "assegn. spost.: copie = " << Buffer::element_copies << ", b.size = " << b.size()
              << ", d.size = " << d.size() << '\n';

    Buffer e;
    e = c;                                                            // assegnamento di COPIA (c e' un lvalue)
    std::cout << "assegn. copia: copie = " << Buffer::element_copies << '\n';

    Buffer f = make(500);                                             // temporaneo: spostamento o elisione, nessuna copia
    std::cout << "da funzione: copie = " << Buffer::element_copies << ", f.size = " << f.size() << '\n';

    std::vector<Buffer> v;                                            // il vector, riallocando, sposta se e' noexcept
    long prima = Buffer::element_copies;
    for (int i = 0; i < 5; ++i) v.push_back(Buffer{100});            // rvalue: push_back(Buffer&&)
    std::cout << "5 push_back di temporanei: copie aggiunte = " << Buffer::element_copies - prima << '\n';
}

Teoria collegata