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” | |
L'oggetto non ha uno stato di default sensato |
| Copia alla creazione | |
La copia rompe la semantica (unicità, ownership, identità) |
| Copia in un oggetto esistente | |
Di solito si vieta insieme al costruttore di copia |
| Overload specifico | |
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.
GO TO FULL VERSION