CodeGym /Corsi /C++ SELF /`=delete` — divieti di copia come protezione del design

`=delete` — divieti di copia come protezione del design

C++ SELF
Livello 45, Lezione 1
Disponibile

1. Perché serve =delete

Quando vedi per la prima volta =delete, la mente facilmente scivola verso «ah, è come delete p; — ora liberiamo memoria». E qui C++ tende a giocare uno scherzo: sono due “delete” diversi, solo la parola è uguale. delete p; è l'operatore per liberare memoria dinamica (e abbiamo stabilito che nel codice applicativo cerchiamo di evitarlo). Invece =delete è un modo per dire al compilatore: «La funzione esiste come parte dell'interfaccia, ma non può essere chiamata».

Di fatto, =delete è il tuo modo di inserire un «pulsante rosso» direttamente nel tipo: se qualcuno prova a fare l'operazione vietata, il programma semplicemente non si compilerà. Ed è ottimo, perché il compilatore intercetta l'errore prima che l'utente incontri un segmentation fault (o che il tuo team lead ti fermi nel corridoio).

Immaginate una semplice immagine mentale: la funzione è dichiarata, parte della risoluzione degli overload, ma marcata come "deleted". Se il codice tenta di usarla, il compilatore produce un errore del tipo «use of deleted function».

Questa idea è importante per due motivi. Primo, puoi vietare non solo costruttori/copie, ma anche funzioni normali e overload. Secondo, una funzione deleted a volte fornisce una diagnostica più chiara rispetto a «ops, i tipi non corrispondono, ho scelto un overload strano e ora tutto è rotto».

La standard library usa anch'essa ampiamente funzioni deleted per proibire operazioni prive di senso o pericolose (per esempio, alcuni tipi hanno costruttori di default deleted). Nei draft dello standard C++ tali posti ricorrono regolarmente — per esempio, potresti incontrare situazioni come atomic_ref() = delete; come divieto esplicito di uno scenario d'uso.

2. Vietare la creazione di un oggetto “vuoto”

Un problema molto comune per i principianti: si crea un tipo e poi si scopre che «un oggetto senza dati» non ha senso, però il linguaggio per default permette di scrivere T t{};. Di conseguenza compaiono entità in uno stato strano: sembra che l'oggetto esista, ma non si possa usare — come un «account senza login», solo peggio, perché il compilatore non conosce la tua esperienza pratica.

Se un oggetto deve avere un valore obbligatorio, vieta il costruttore di default.

#include <iostream>

struct TaskId {
    int value{};

    TaskId() = delete;                 // vietiamo id "vuoto"
    explicit TaskId(int v) : value(v) {}
};

int main() {
    TaskId id{10};
    std::cout << id.value << '\n';     // 10
}

Nota l'effetto psicologico: il tipo riferisce da sé le regole del gioco. Non devi sperare che tutti abbiano letto il commento «non si può creare senza id». I commenti si leggono raramente. Il compilatore — sempre.

Incorporiamo ora questo nel nostro esempio didattico. Supponiamo di avere un semplice modello di task (micro-todo manager), che sviluppiamo ancora dall'epoca in cui usavamo struct, vector e funzioni.

#include <string>

struct Task {
    int id{};
    std::string title;
    bool done{false};
};

Un task con id=0 può essere valido o no — dipende dalle tue regole. Ma se decidi che l'id deve essere strettamente positivo e fornito alla creazione, anche Task() può essere proibito (o puoi rendere un costruttore che richiede id e title).

3. Vietare la copia: tipi non-copyable

Le cose migliori arrivano quando vieti la copia. Qui è importante non confondere: vietare la copia non è una «ottimizzazione», ma prima di tutto un fatto di semantica. Ci sono tipi che per semantica non si possono copiare, perché la copia distruggerebbe il modello del mondo.

I casi classici sono: l'oggetto possiede qualcosa di unico (un file, una connessione, un «generatore di id unico», una «sessione attiva unica»). Se copi un tale oggetto, improvvisamente avrai «due unici» proprietari, e questo suona come cattiva filosofia e ancora peggio ingegneria.

Come vietare la copia

La sintassi tipica per vietare la copia è: rimuovere il costruttore di copia e l'assegnazione di copia.

#include <iostream>

struct IdGenerator {
    int nextId{1};

    IdGenerator() = default;

    IdGenerator(const IdGenerator&) = delete;
    IdGenerator& operator=(const IdGenerator&) = delete;

    int allocate() {
        return nextId++;
    }
};

int main() {
    IdGenerator gen;
    std::cout << gen.allocate() << '\n'; // 1
    std::cout << gen.allocate() << '\n'; // 2
}

Perché questo è utile nella nostra app? Perché il generatore di id è un ottimo esempio di entità «unica». Se viene copiato accidentalmente, due copie cominceranno a fornire gli stessi id, e il tuo database di task diventerà un festival di collisioni: «task 7» apparirà più spesso della parola «compiler» in una lezione su C++.

=delete e le firme delle funzioni: di solito per valore serve una copia

Quando vieti la copia, emerge una pratica importante: non tutte le firme delle funzioni sono compatibili con tipi non-copyable. Non è un «limite del linguaggio», ma la conseguenza diretta della semantica: se un tipo non è copiabile, una funzione che lo accetta per valore diventa automaticamente sospetta.

Mostriamo sul nostro IdGenerator. Se vogliamo scrivere una funzione che «mostri l'attuale nextId», non c'è bisogno di copiare il generatore — basta una reference.

#include <iostream>

struct IdGenerator {
    int nextId{1};
    IdGenerator(const IdGenerator&) = delete;
    IdGenerator& operator=(const IdGenerator&) = delete;
};

void printState(const IdGenerator& gen) {
    std::cout << gen.nextId << '\n';
}

int main() {
    IdGenerator gen;
    printState(gen); // 1
}

E questo è qualcosa che non si può fare (e giustamente):

// void printState(IdGenerator gen); // richiederebbe la copia: errore

Qui il compilatore diventa il tuo alleato: non ti permetterà di progettare accidentalmente un'API come se il tipo fosse copiabile. Devi essere onesto: o const& per la lettura, o & per la modifica.

Vietare la copia senza “buchi”: rimuovere entrambe le operazioni

C'è un bug di design molto comune: vietare il costruttore di copia, ma dimenticare di vietare l'assegnazione di copia (o viceversa). Così il tipo diventa «metà vietato»: non si può creare una copia, ma si può assegnare a un oggetto esistente — e questo può essere inutile, pericoloso o semplicemente strano.

Vediamo un esempio.

struct Weird {
    int x{};

    Weird(const Weird&) = delete;      // vietata la copia alla creazione
    // Weird& operator=(const Weird&) = ???  // dimenticato
};

Ora Weird b = a; è vietato, ma b = a; potrebbe inaspettatamente essere permesso (se il compilatore genera l'operatore di assegnazione). Sembra la regola «non copiare, ma se proprio vuoi, puoi». Di solito non è l'intenzione.

Perciò, in pratica didattica e reale, vale spesso la regola semplice: se un tipo è non-copyable per semantica, rimuovi sia il costruttore di copia sia l'assegnazione di copia. Così non rimangono “buchi”.

4. Gestire l'interfaccia: rimuovere overload indesiderati

Finora abbiamo usato =delete come strumento per funzioni speciali (costruttori/copie). Ma è ugualmente utile per metodi “normali”, quando vuoi vietare una forma specifica di chiamata.

Un esempio concreto: scrivi una funzione setTitle, e per te è importante che il titolo sia esattamente uno std::string, non un raw const char*. Perché? Per esempio, perché vuoi distinguere esplicitamente la «stringa come oggetto» dal «puntatore a memoria dove giacciono caratteri». Oppure perché stai facendo overload e non vuoi conversioni implicite indesiderate.

Creiamo un piccolo componente per la nostra app — un “editor task”.

#include <string>

struct TaskEditor {
    std::string title;

    void setTitle(const std::string& s) {
        title = s;
    }

    void setTitle(const char*) = delete; // vietiamo questa forma di chiamata
};

Ora editor.setTitle("hi"); non si compila, anche se il literal poteva tecnicamente adattarsi. Invece editor.setTitle(std::string{"hi"}); — ok. Sembra più rigoroso, ma a volte il rigore impedisce semplicemente bug futuri.

Importante: non è una raccomandazione universale «vietate sempre const char*». È un esempio di come =delete possa governare l'interfaccia, non solo il ciclo di vita.

5. Esempio: TaskRepository non copiabile

Assemblamo un piccolo ma coerente frammento del “todo-app”, dove =delete protegge realmente il design.

L'idea: abbiamo un repository di task TaskRepository, che contiene internamente uno std::vector<Task> e un IdGenerator. Per semantica il repository è il “centro dei dati”; copiarlo accidentalmente nei parametri o ritornarlo per valore (senza discutere lo spostamento) è una cattiva idea: potresti ottenere due copie indipendenti della lista di task e divergente stato.

Rendiamo il repository non-copyable:

#include <string>
#include <vector>

struct Task {
    int id{};
    std::string title;
    bool done{false};
};

struct IdGenerator {
    int nextId{1};

    IdGenerator(const IdGenerator&) = delete;
    IdGenerator& operator=(const IdGenerator&) = delete;

    int allocate() { return nextId++; }
};

struct TaskRepository {
    std::vector<Task> tasks;
    IdGenerator gen;

    TaskRepository() = default;

    TaskRepository(const TaskRepository&) = delete;
    TaskRepository& operator=(const TaskRepository&) = delete;

    void addTask(const std::string& title) {
        tasks.push_back(Task{gen.allocate(), title, false});
    }
};

Notare che non liberiamo nulla manualmente, non scriviamo distruttori e non usiamo new/delete. Diciamo semplicemente: «Non si può copiare». Questa è una sana filosofia del C++ moderno: lascia che i componenti standard svolgano il lavoro pesante e tu descrivi la semantica e i vincoli.

Verifichiamo nel main che tutto funzioni.

#include <iostream>
#include <string>
#include <vector>

int main() {
    TaskRepository repo;
    repo.addTask("Write C++ code");
    repo.addTask("Read compiler errors");

    std::cout << repo.tasks.size() << '\n'; // 2
}

Se qualcuno poi proverà a fare TaskRepository copy = repo;, il compilatore bloccherà tutto — e risparmierai ore di debugging sul «perché la lista dei task si è duplicata».

6. Quando applicare =delete

Quando usare =delete e quando lasciare le cose come sono? Una buona euristica: serve dove l'uso scorretto del tipo è probabile e costoso, e l'uso corretto può essere espresso esplicitamente.

Un semplice schema da tenere a mente:

flowchart TD
    A[Vuoi vietare l'operazione?] --> B{L'operazione è semanticamente scorretta?}
    B -->|Si| C[Vietare tramite =delete]
    B -->|No| D{L'operazione è indesiderata ma talvolta utile?}
    D -->|Si| E[Non vietare: cercare un altro design o documentare]
    D -->|No| F[Lasciare com'e']

Qui conta «semanticamente scorretta». Per esempio, «creare un oggetto senza l'id obbligatorio» è spesso scorretto. «Copiare il proprietario di una risorsa unica» è scorretto. «Vietare tutti i costruttori per sicurezza» è di solito esagerato.

Divieto esplicito e divieto “derivato dai campi”: due fonti di non-copyable

A volte uno studente vede l'errore di compilazione «copying deleted» e pensa: «Ma io non ho scritto =delete!». Questo è normale: l'operazione può essere resa non disponibile automaticamente a causa dei campi del tipo.

Per esempio, se hai un campo di tipo std::unique_ptr, la copia dell'intero tipo sarà proibita perché unique_ptr non è copiabile. Questo è utile: l'unico ownership rimane unico.

Non entreremo nei dettagli, ma è importante capire la differenza leggendo il codice: il divieto può essere «esplicitamente dichiarato» con =delete, oppure può «emergere» dai campi. In entrambi i casi il risultato è lo stesso: il compilatore proibisce l'operazione. La differenza è quanto l'intenzione è evidente dall'interfaccia.

In molti team si preferisce scrivere =delete anche quando un campo rende già il tipo non-copyable, così l'intento è visibile subito e non costringe il lettore a «indagare i campi».

Mini-tabella: cosa si cancella più spesso

Per fissare, riepiloghiamo i divieti tipici in una piccola tabella. Non è una «legge del C++», ma un insieme pratico di pattern.

Cosa vietare Come si scrive Quando è opportuno
Creazione di un oggetto “vuoto”
T() = delete;
L'oggetto non ha uno stato di default sensato
Copia alla creazione
T(const T&) = delete;
La copia rompe la semantica (unicità, ownership, identità)
Copia in un oggetto esistente
T& operator=(const T&) = delete;
Di solito si vieta insieme al costruttore di copia
Overload specifico
void f(int) = delete;
Vogliamo proibire una variante di chiamata pericolosa/indesiderata

7. Errori tipici lavorando con =delete

Errore #1: confondere =delete e delete p;.
delete p; è per liberare memoria dinamica. =delete è per l'interfaccia e per proibire la chiamata di una funzione. Confondere questi concetti porta a conclusioni strane come «ho vietato il costruttore, quindi la memoria verrà liberata» (spoiler: no).

Errore #2: vietare solo il costruttore di copia ma lasciare l'assegnazione di copia.
Allora il tipo diventa “stranamente semi-copiabile”. Nel migliore dei casi ottieni un'interfaccia confusa, nel peggiore una copia inattesa dello stato dove non te l'aspetti. Se la copia è vietata per semantica, nella maggior parte dei casi si rimuovono entrambe le operazioni: sia T(const T&) sia operator=.

Errore #3: rimuovere il costruttore di default e dimenticare di fornire un modo valido per creare l'oggetto.
Questo è come una porta senza maniglia: «l'oggetto non si può creare in alcun modo». Quando vieti T(), assicurati che esista almeno un costruttore che crei un oggetto valido (per esempio explicit T(int)).

Errore #4: provare a passare un tipo non-copyable per valore.
Un parametro per valore quasi sempre implica una copia (o il tentativo di farla). Per tipi non-copyable bisogna abituarsi a const T& per la lettura e T& per la modifica. All'inizio è strano, ma poi inizi a progettare interfacce “per semantica” e non per inerzia.

Errore #5: usare =delete “per sicurezza” senza una motivazione chiara.
Il divieto è parte del contratto del tipo. Se non sai spiegare perché l'operazione è vietata (unicità, ownership, stato senza senso, overload pericoloso), c'è una buona probabilità che il divieto complichi l'uso del tipo e spinga le persone a cercare workaround invece di adottare un design corretto.

1
Compito
C++ SELF, livello 45, lezione 1
Bloccato
Accesso al sistema
Accesso al sistema
1
Compito
C++ SELF, livello 45, lezione 1
Bloccato
Filtro di testo
Filtro di testo
1
Compito
C++ SELF, livello 45, lezione 1
Bloccato
Contatore del tornello
Contatore del tornello
1
Compito
C++ SELF, livello 45, lezione 1
Bloccato
Classifica
Classifica
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION