1. Introduzione
Quando una classe diventa anche solo un po' utile, succede che non è comodo crearla «in un solo formato». L'utilizzatore della classe (anche voi dopo una settimana) vuole ora creare un oggetto con tutti i dati, ora con i minimi, ora un oggetto "vuoto" di riserva. E qui viene voglia di scrivere 3–4 costruttori… e comincia il copy-paste.
Immaginate di scrivere una classe Task per uno scheduler da linea di comando (il nostro progetto didattico): un task ha un titolo e una priorità. Un costruttore è la «forma completa», il secondo è «titolo, priorità di default», il terzo è «task vuoto». Se in ciascuno di essi ripetete manualmente gli stessi controlli e assegnazioni, prima o poi si disallineeranno. Non è filosofia, è statistica.
Per sentire il problema, guardiamo la versione «naive»:
#include <string>
class Task {
std::string title_;
int priority_;
public:
Task(std::string title, int priority) {
if (title.empty()) title = "untitled";
if (priority < 1) priority = 1;
if (priority > 5) priority = 5;
title_ = title;
priority_ = priority;
}
Task(std::string title) {
if (title.empty()) title = "untitled";
int priority = 3;
title_ = title;
priority_ = priority;
}
};
Il codice non è ancora «orrendo», ma somiglia sospettosamente a copy-paste. Ora immaginate di aver aggiunto una terza regola: «rimuovere gli spazi ai bordi» o «vietare titoli più lunghi di 80». L'avete aggiunta in un costruttore, l'avete dimenticata nell'altro… ed è il caos.
2. Costruttori deleganti: idea e ordine di esecuzione
Idea in parole semplici
Un costruttore delegante è un modo per dire al compilatore: «voglio creare l'oggetto in questo modo, mentre un altro costruttore è solo un comodo involucro». Quindi un costruttore diventa quello principale (centrale), e gli altri sono sottili «alias» che reindirizzano la chiamata al principale.
Per sensazione è molto simile a una buona abitudine nelle funzioni: non spargere la logica in 5 posti, ma avere una funzione che fa il lavoro e alcune piccole funzioni che preparano gli argomenti e la chiamano. Qui parliamo di costruttori.
La sintassi della delegazione è così:
ClassName(args_for_this_ctor) : ClassName(args_for_other_ctor) {
// corpo (eseguito DOPO il costruttore delegato)
}
Sì, un costruttore chiama un altro costruttore della stessa classe. E no, non è ricorsione «a serpente che si morde la coda» — se fatto correttamente, la catena termina nel costruttore «principale» che inizializza effettivamente i campi.
Regola principale e ordine di esecuzione
Quando vedete per la prima volta la delegazione è facile immaginare una scena sbagliata: come se entrambi i costruttori «insieme» inizializzassero i campi. In realtà il modello è più semplice e rigoroso: esattamente un costruttore è responsabile della creazione dei campi, e l'altro si limita a reindirizzare la chiamata.
Un costruttore delegante ha un comportamento chiave: prima viene eseguito il costruttore verso cui abbiamo delegato (è quello che inizializza l'oggetto), e solo dopo viene eseguito il corpo del costruttore delegante (se presente).
Ecco uno schema compatto:
flowchart TD
A["Task(title)"] --> B[": Task(title, 3)"]
B --> C["Task(title, priority) inizializza i campi"]
C --> D["Corpo di Task(title) (se presente)"]
Se ricordate questa immagine, eviterete metà degli errori tipici.
3. Esempi: Rectangle e Task
Rectangle: il quadrato come caso particolare del rettangolo
Prima di toccare il nostro progetto, riscaldiamoci con un esempio che non richiede contesto. Un rettangolo ha larghezza e altezza. Il caso particolare è il quadrato, dove larghezza e altezza sono uguali. La cosa più stupida è duplicare l'inizializzazione in due costruttori. La cosa migliore è delegare.
class Rectangle {
int w_;
int h_;
public:
Rectangle(int w, int h) : w_(w), h_(h) {}
explicit Rectangle(int side) : Rectangle(side, side) {}
};
Nota due aspetti.
Primo, il costruttore Rectangle(int side) non inizializza w_ e h_ direttamente — delega al «principale» Rectangle(int w, int h).
Secondo, il codice è molto leggibile: si vede subito l'intenzione dell'autore — «il quadrato si crea come rettangolo con lati side, side». È come un commento, ma il compilatore assicura anche che non rompiate la logica.
Esempio pratico: classe Task per un TaskTracker da console
Ora rendiamo l'esempio più vicino alla realtà: un task nella nostra ipotetica applicazione TaskTracker a riga di comando. Anche se nel vostro corso il progetto ha un nome diverso — non importa: conta l'idea «modello + creazione corretta».
Supponiamo che il task abbia un titolo e una priorità da 1 a 5. Il nostro obiettivo è fare in modo che ogni task dopo la creazione sia valido: titolo non vuoto, priorità nel range.
Versione senza delegazione
Prima il modo «come spesso si fa»:
#include <string>
class Task {
std::string title_;
int priority_;
public:
Task(std::string title, int priority) {
if (title.empty()) title = "untitled";
if (priority < 1) priority = 1;
if (priority > 5) priority = 5;
title_ = title;
priority_ = priority;
}
Task(std::string title) {
if (title.empty()) title = "untitled";
int priority = 3;
title_ = title;
priority_ = priority;
}
};
Il problema non è solo il copy-paste. C'è anche un problema «silenzioso»: i campi vengono prima creati «come capita» e poi li assegniamo. Per un int non è grave, ma per tipi più complessi (stringhe, vettori) perdiamo parte del senso dell'inizializzazione: l'oggetto è già stato creato e noi lo «vestiamo» dopo.
Costruttore principale + delegazione
Facciamo il costruttore principale che prende tutto, normalizza e inizializza i campi. Gli altri delegano.
#include <string>
#include <utility>
class Task {
std::string title_;
int priority_;
static int clamp_priority(int p) {
if (p < 1) return 1;
if (p > 5) return 5;
return p;
}
static std::string normalize_title(std::string t) {
if (t.empty()) return "untitled";
return t;
}
public:
Task(std::string title, int priority)
: title_(normalize_title(std::move(title)))
, priority_(clamp_priority(priority))
{}
explicit Task(std::string title)
: Task(std::move(title), 3)
{}
Task()
: Task("untitled", 3)
{}
};
Ci sono diverse belle cose qui, ma la principale è che le regole di validità vivono in un unico posto: nel costruttore principale e nei suoi piccoli helper. Volete cambiare il range delle priorità a 1..10? Modificate clamp_priority e tutta l'applicazione si comporterà allo stesso modo.
Un'altra cosa utile: i costruttori deleganti qui sono «sottili». Non cercano di ripetere nel corpo ciò che farà il costruttore principale. Si limitano a preparare gli argomenti.
Controllo rapido in main()
Aggiungiamo un piccolo main() per vedere che tutto funziona.
#include <iostream>
#include <string>
int main() {
Task a; // title="untitled", priority=3
Task b{"", 100}; // title="untitled", priority=5
Task c{"Read a book"}; // title="Read a book", priority=3
std::cout << "Tasks created.\n"; // Tasks created.
}
Per ora non stampiamo i campi (non abbiamo ancora operator<< per la classe — lo vedremo nelle prossime lezioni), ma è importante che la creazione non fallisca e non richieda un «secondo passo» di configurazione.
Corpo del costruttore delegante: quando serve
Nella maggior parte dei casi un costruttore delegante dovrebbe essere «quasi vuoto». Ma a volte si vuole aggiungere una piccola logica dopo la delegazione — per esempio logging (a scopo didattico) o una semplice diagnostica.
È importante capire: nel costruttore delegante il corpo viene eseguito dopo che il costruttore principale ha fatto il suo lavoro e l'oggetto è già inizializzato. Questo significa che nel corpo potete accedere ai campi in sicurezza: sono già in uno stato consistente.
Per esempio:
#include <iostream>
#include <string>
#include <utility>
class Task {
std::string title_;
int priority_;
public:
Task(std::string title, int priority)
: title_(title.empty() ? "untitled" : std::move(title))
, priority_(priority < 1 ? 1 : (priority > 5 ? 5 : priority))
{}
Task(std::string title)
: Task(std::move(title), 3)
{
std::cout << "Task created with default priority.\n";
// Task created with default priority.
}
};
In produzione reale non stampereste dal costruttore sulla console (l'utente non deve sapere che avete delegato internamente), ma per didattica è un buon modo per sentire l'ordine di esecuzione.
Delegazione e invarianti: stesse regole senza disallineamenti
Il costruttore è il punto d'ingresso in uno stato valido. Ma se avete 3 costruttori e ognuno normalizza i dati «a modo suo», in pratica avete non una classe ma tre versioni diverse della stessa entità. Poi beccate bug che sembrano magia: «perché il task creato con Task() si comporta diversamente rispetto a quello creato con Task(title)?»
La delegazione cura questo obbligandovi a scegliere un unico luogo dove definire le regole. Anche se non siete fan della teoria, è pratico: meno posti da modificare — meno probabilità di dimenticanze.
Piccola tabella «prima / dopo»:
| Approccio | Cosa succede | Rischi |
|---|---|---|
| Più costruttori «indipendenti» | Ognuno con i propri controlli e assegnazioni | Disallineamento regole, copy-paste, difficile da modificare |
| Costruttore principale + delegazione | Un costruttore definisce le regole, gli altri preparano i parametri | Codice più corto, modifiche in un unico posto |
4. Limitazioni e anti-esempi
I costruttori deleganti sono potenti, ma non «magici». Hanno alcune regole, che è meglio accettare come «leggi della fisica» piuttosto che cercare di aggirarle.
Un costruttore delegante non può contemporaneamente delegare a un altro costruttore e inizializzare separatamente dei campi nella lista di inizializzazione. In pratica non si può scrivere «delego e nel frattempo aggiusto un campo». La logica è: se deleghi, passi totalmente la responsabilità dell'inizializzazione.
Un altro punto importante: la delegazione deve avere una destinazione finale. Se create una catena che gira in tondo, il compilatore non «capirà cosa intendevate» e vi segnalerà l'errore.
Brutto esempio (da non fare):
class Bad {
public:
Bad() : Bad(0) {} // delega
Bad(int) : Bad() {} // delega indietro (ciclo)
};
È concettualmente simile a «vado al negozio per il pane, torno a casa per la lista e ho lasciato la lista al negozio». Storia interessante, ma pane non ne avrete.
5. Errori tipici
Errore №1: il copy-paste dei controlli rimane, solo ora è “ben impacchettato”.
A volte lo studente scrive costruttori deleganti ma continua a lasciare controlli e normalizzazioni in ciascuno «per sicurezza». Otteniamo due problemi insieme: c'è delegazione e il disallineamento è ancora possibile. Buono stile: le regole stanno in un unico costruttore principale, gli altri solo preparano argomenti e passano avanti.
Errore №2: tentativo di “pre-inizializzare” i campi nella lista di inizializzazione del costruttore delegante.
Dopo l'introduzione della delegazione nasce la tentazione di scrivere: Task() : Task("untitled", 3), priority_(5) {} — «tanto cambio solo la priorità». Non è permesso: o deleghi o inizializzi i campi direttamente. Se serve cambiare valori, modificate gli argomenti della delegazione o fatelo nel corpo (se ha senso e non rompe gli invarianti).
Errore №3: ciclo di delegazione.
Il ciclo può essere evidente (come nell'esempio Bad), oppure nascosto in una catena di tre costruttori. Il compilatore non lo approverà. Regola pratica: la catena di delegazione deve avere una «fermata finale» — il costruttore che inizializza davvero i campi e non delega oltre.
Errore №4: tentare di eseguire logica importante «prima della delegazione».
A volte si vuole «calcolare qualcosa prima e poi delegare». Ma la delegazione è scritta nella lista di inizializzazione, che viene eseguita prima del corpo del costruttore. Quindi, se dovete preparare parametri, fatelo con funzioni static / helper (come normalize_title), oppure riprogettate il costruttore principale affinché riceva dati già pronti.
Errore №5: trasformare la delegazione in una “torta a strati” piena di effetti collaterali.
Se nei costruttori deleganti cominciano ad apparire stampe, scritture su file, modifiche di oggetti esterni e altri effetti, l'ordine delle chiamate diventa troppo importante e il codice fragile. A scopo didattico std::cout è accettabile, ma come abitudine di progettazione è meglio mantenere i costruttori deleganti il più possibile sottili e prevedibili.
GO TO FULL VERSION