1. Introduzione
Quando un principiante vede & in un parametro, la reazione iniziale spesso è: «Oh no, è qualcosa di pericoloso, probabilmente uno di quei temi spaventosi dove tutto va in crash». In realtà i riferimenti nei parametri sono uno degli strumenti più amichevoli di C++… se li consideri come un contratto, e non come un «trucco».
Immagina che una funzione sia una piccola officina. Gli argomenti sono ciò che porti dentro. Ci sono due modi di lavorare: o porti una copia del pezzo (passaggio per valore), oppure porti il pezzo stesso e dici «eccolo, lavora direttamente su di lui» (passaggio per riferimento). Un riferimento è come un badge «sono il rappresentante ufficiale di questo oggetto».
T& come «secondo nome»
Un avvio dolce importante: un riferimento non è un nuovo oggetto. Un riferimento è un altro nome per un oggetto già esistente.
Quindi se hai una variabile a, e ottieni da qualche parte int& r = a;, allora r e a sono «due targhette sulla stessa porta». Dentro la stanza c'è lo stesso oggetto.
Esempio minimo, per sentire con mano:
#include <iostream>
int main() {
int a = 10;
int& r = a; // r — secondo nome di a
r += 5;
std::cout << a << '\n'; // 15
}
Qui non c'è copia. Non c'è un «secondo int». C'è un solo int, ma ci sono due vie per riferirsi ad esso.
Parametro T&: la funzione può modificare l'argomento
Ora trasferiamo l'idea alle funzioni. Quando un parametro è scritto come T&, si legge: «La funzione lavora sull'oggetto reale del chiamante». Quindi, le modifiche dentro la funzione saranno visibili dall'esterno. Non è un «effetto collaterale», è proprio il senso di questo tipo di parametro: hai esplicitamente permesso alla funzione di modificare il tuo oggetto.
L'esempio più breve è l'incremento:
#include <iostream>
void inc(int& x) {
x += 1;
}
int main() {
int a = 10;
inc(a);
std::cout << a << '\n'; // 11
}
Qui x è a. Quindi x += 1 modifica a.
E qui entra in gioco una regola di leggibilità molto adulta: se usi T&, dal nome della funzione dovrebbe risultare in genere che essa modifica l'argomento. inc, append, normalize, sort_inplace sono buoni nomi. calc, get, print sono sospetti, perché «calc» di solito calcola, non riscrive la tua variabile.
2. T& non crea una copia
Un altro motivo per amare i riferimenti è che spesso evitano copie inutili di oggetti grandi. Ma oggi ci concentriamo non sulla performance fine a se stessa, ma sul significato: se una funzione deve modificare una stringa — ha senso che accetti un riferimento.
Per esempio, aggiungiamo un punto esclamativo alla fine di una stringa:
#include <iostream>
#include <string>
void add_exclamation(std::string& s) {
s += "!";
}
int main() {
std::string name = "Bob";
add_exclamation(name);
std::cout << name << '\n'; // Bob!
}
Dettaglio importante: se il parametro fosse std::string s (per valore), modificheresti una copia e all'esterno non succederebbe nulla. Il tipo del parametro non è «come è più comodo», ma cosa la funzione promette.
3. Limitazione di T&: non si può legare a un valore temporaneo
Ora arriva il punto in cui molti ricevono per la prima volta un errore di compilazione e pensano: «Il compilatore è impazzito». No, il compilatore ti ha salvato.
Non si può legare T& a un valore temporaneo (a un oggetto che vive pochissimo).
Esempio:
#include <string>
void add_exclamation(std::string& s) {
s += "!";
}
int main() {
add_exclamation(std::string("Bob")); // errore di compilazione
}
Perché? Perché std::string("Bob") è un oggetto temporaneo. Appare «per la durata dell'espressione» e poi scompare. Se C++ ti permettesse di creare su di esso un riferimento «di lunga durata», potresti cominciare a modificare un oggetto che sta per evaporare. È come cercare di fissare una mensola all'aria: finché tieni il trapano sembra che ci sia dove avvitare, ma poi la parete non c'era.
Regola pratica: T& richiede una «variabile reale» (oggetto nominato), non il risultato di un'espressione temporanea.
4. const T&: leggo senza copia e prometto «non modifico»
Adesso il secondo protagonista della lezione: const T&.
Quando scrivi const T&, stai facendo due cose insieme:
- dici «non voglio copiare, dammi accesso all'oggetto originale»
- dici «e non mi permetterò di modificarlo — promessa da programmatore onesto»
Questo è un tipo di parametro molto comune per funzioni che leggono soltanto l'oggetto, specialmente se l'oggetto può essere grande (stringa, vettore).
Esempio: funzione che stampa una stringa:
#include <iostream>
#include <string>
void print_line(const std::string& s) {
std::cout << s << '\n';
}
int main() {
std::string msg = "hello";
print_line(msg); // stampa: hello
}
Idea chiave: print_line non ha il diritto di modificare s. Se dentro scrivi s += "!", il compilatore ti fermerà. È un modo diretto e solido per proteggerti da corruzioni accidentali dei dati.
A proposito, nella libreria standard l'idea di «reference/const_reference» è un modo consolidato per nominare rispettivamente «riferimento a elemento» e «riferimento costante a elemento» (per esempio nei container), e perfino nelle note redazionali allo standard si incontrano formulazioni che parlano di (const_)reference come pattern di denominazione standard.
const T& può legarsi a un valore temporaneo
Qui const T& batte T& in comodità. const T& può legarsi a un oggetto temporaneo. Ha senso: se prometti «non modifico», fare riferimento a un temporaneo è più sicuro, perché non lo modifichi e di solito lo usi «subito».
Esempio:
#include <iostream>
#include <string>
std::size_t len(const std::string& s) {
return s.size();
}
int main() {
std::cout << len(std::string("abc")) << '\n'; // 3
}
Questa è una comodità gradevole: puoi passare il risultato di un'espressione senza variabili temporanee aggiuntive, e al tempo stesso evitare copie della stringa.
Importante: non trasformare questo in una religione. Per tipi piccoli (int, double) const T& è spesso eccessivo, e nella lezione 71 abbiamo discusso perché «ovunque const int&» è rumore nel codice. Ma per stringhe e vettori è generalmente stile corretto.
5. Mini-tabella: T, T& o const T&
Ora è utile mettere tutto in una tabella compatta, così da smettere di «indovinare» e cominciare a scegliere consapevolmente.
| Cosa vuoi per significato | Cosa scrivere nel parametro | Cosa significa |
|---|---|---|
| «Ho bisogno di una copia, lavoro con un valore indipendente» | |
viene creata una copia dell'argomento |
| «Modificherò il tuo oggetto» | |
lavoro con l'oggetto originale, le modifiche sono visibili all'esterno |
| «Leggo solo, non voglio copia» | |
lavoro con l'oggetto originale, ma è proibito modificarlo |
Questa tabella non è su «ottimizzazione». È per evitare che le firme diventino sorprese.
Schema: come funziona il «secondo nome»
Quando ti abitui ai riferimenti, aiuta mantenere un modello visivo: T& non è una scatola, è una freccia verso lo stesso oggetto.
flowchart LR
A["Variabile a (oggetto reale)"] -->|alias| R["Alias r (secondo nome)"]
A -->|alias| P["Parametro x di tipo T& nella funzione"]
Senso: c'è un solo oggetto (a), ma possono esserci più nomi/percorsi di accesso (r, x).
6. Pratica: riferimenti in un TodoList da console
Facciamo un passo pratico: aggiungiamo alla nostra applicazione (un semplice TodoList da console) le funzioni in modo che dalle firme sia chiaro dove modifichiamo la lista di task e dove la leggiamo soltanto.
Immaginiamo che i task siano semplicemente un std::vector<std::string>. Per ora non modelliamo il «task» come struct (lo vedremo più avanti nel corso), quindi manteniamo tutto il più semplice possibile.
Stampare i task: const std::vector<std::string>&
Cominciamo dalla stampa. Stampare non dovrebbe modificare la lista. Quindi — const&.
#include <iostream>
#include <string>
#include <vector>
void print_tasks(const std::vector<std::string>& tasks) {
std::cout << "Tasks: " << tasks.size() << '\n';
for (std::size_t i = 0; i < tasks.size(); ++i) {
std::cout << i << ": " << tasks[i] << '\n';
}
}
Nota: non copiamo tasks, lo leggiamo. Se provassi accidentalmente a fare tasks.push_back("..."), il compilatore ti fermerebbe — ed è una cosa buona.
Aggiungere un task: modifichiamo la lista (T&), leggiamo il testo (const T&)
Aggiungere un task modifica il vettore, quindi il vettore va passato per riferimento &. Il testo del task lo leggiamo solo, quindi la stringa è const&.
#include <string>
#include <vector>
void add_task(std::vector<std::string>& tasks, const std::string& text) {
if (!text.empty()) {
tasks.push_back(text);
}
}
La firma si legge quasi come una frase: «modifico tasks, ma non tocco text».
Normalizzare una stringa «in loco»: esempio di onesto std::string&
Compito comune: sistemare la stringa «in loco», per esempio rimuovere spazi in eccesso ai bordi. Non faremo un trim perfetto per tutti i casi (è un argomento a sé), faremo una versione didattica: se la stringa inizia con uno spazio — rimuovi uno spazio; se finisce con uno spazio — rimuovi uno spazio.
#include <string>
void trim_one_space(std::string& s) {
if (!s.empty() && s.front() == ' ') {
s.erase(0, 1);
}
if (!s.empty() && s.back() == ' ') {
s.pop_back();
}
}
E l'uso:
#include <iostream>
#include <string>
int main() {
std::string t = " hello ";
trim_one_space(t);
std::cout << t << '\n'; // hello
}
Qui std::string& è assolutamente giustificato: la funzione sta proprio modificando la stringa.
Parametro «out» tramite T& A volte vuoi che una funzione non solo «calcoli e restituisca», ma scriva il risultato in una variabile già esistente. Questo si chiama parametro di output. Oggi non approfondiamo il design «come è meglio restituire più valori», ma il meccanismo con il riferimento vale la pena mostrarlo.
Esempio: la funzione cerca il primo task che contiene una sottostringa. Se trova — scrive l'indice in out_index e ritorna true, altrimenti ritorna false.
#include <string>
#include <vector>
bool find_task(const std::vector<std::string>& tasks,
const std::string& query,
std::size_t& out_index) {
for (std::size_t i = 0; i < tasks.size(); ++i) {
if (tasks[i].find(query) != std::string::npos) {
out_index = i;
return true;
}
}
return false;
}
Esempio di utilizzo:
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> tasks{"buy milk", "learn c++", "sleep"};
std::size_t idx = 0;
if (find_task(tasks, "c++", idx)) {
std::cout << idx << '\n'; // 1
}
}
Il senso di std::size_t& out_index è semplice: «se ho trovato — scrivo la risposta qui». Di nuovo, il contratto si legge dalla firma.
Se scambi T& e const T&
Punto molto pratico: i principianti spesso mettono T& «per sicurezza», e poi inaspettatamente modificano i dati «dall'esterno» e trovano effetti strani.
Immagina di aver scritto una funzione «stampa i task», ma per sbaglio hai dimenticato il const:
#include <iostream>
#include <string>
#include <vector>
void print_tasks_wrong(std::vector<std::string>& tasks) {
tasks.push_back("oops"); // modificato inaspettatamente il vettore
std::cout << tasks.size() << '\n';
}
Questo è un esempio di contratto cattivo: la funzione dal nome «stampa» in realtà modifica i dati. Cose del genere trasformano il programma in un giallo, dove il colpevole sei tu di due settimane fa.
L'errore inverso accade anche: volevi modificare un oggetto, ma per errore hai scritto const T& — e il compilatore te lo ha impedito. In questo caso è una buona cosa: meglio che il compilatore dia errore subito piuttosto che il programma «silenziosamente faccia la cosa sbagliata».
Mini-scheletro di main.cpp
Per collegare gli esempi in una singola applicazione, ecco un piccolo scheletro dove puoi inserire le funzioni. Non è l'architettura finale, ma assomiglia già a un progetto didattico con una funzione main leggera.
#include <iostream>
#include <string>
#include <vector>
void print_tasks(const std::vector<std::string>& tasks);
void add_task(std::vector<std::string>& tasks, const std::string& text);
int main() {
std::vector<std::string> tasks;
add_task(tasks, "learn references");
add_task(tasks, "drink tea");
print_tasks(tasks);
return 0;
}
E l'output atteso potrebbe essere:
// Tasks: 2
// 0: learn references
// 1: drink tea
7. Errori tipici lavorando con T& e const T&
Errore №1: mettere T& «per sicurezza», anche se la funzione non dovrebbe modificare l'argomento.
Questo è il problema più comune, perché sembra che «il riferimento sia più veloce» e «sia più giusto». Alla fine ottieni una funzione che per contratto può modificare l'argomento, e il lettore del codice è costretto a sospettare un tranello. Se non modifichi l'oggetto — scegli const T& (per tipi grandi) o il passaggio per valore (per tipi piccoli).
Errore №2: provare a legare T& a un valore temporaneo e meravigliarsi dell'errore di compilazione.
Per esempio, f(std::string("hi")) con f(std::string&) non compila. Non è un capriccio del linguaggio, ma una protezione contro il legare un riferimento a un oggetto che scomparirà «in un attimo». Se vuoi accettare temporanei e solo leggere — usa const T&.
Errore №3: pensare che const sia «per bellezza», e toglierlo senza motivo.
const in const T& è un contratto. Non serve solo al lettore, ma anche al compilatore: vieta modifiche accidentali dell'argomento. Se rimuovi const, disattivi letteralmente la cintura di sicurezza, perché «così è più comodo raggiungere il cassetto».
Errore №4: scrivere const T x invece di const T& x, pensando che così «non ci sarà copia».
Il parametro const T x crea comunque una copia — solo che è «immutabile». A volte è utile, ma se l'obiettivo era non copiare std::string/std::vector, allora serve proprio const T&.
Errore №5: scrivere funzioni che modificano inaspettatamente tramite riferimento senza alcun indizio nel nome.
Se una funzione modifica un argomento tramite T&, dovrebbe essere evidente dalla firma (lo si vede già), ma è preferibile che anche il nome lo segnali. Altrimenti il prossimo lettore del codice (incluso il te futuro) lancerà il programma «per sicurezza» e controllerà se non si nasconde un altro sgradito effetto collaterale.
#include <string>
#include <vector>
bool find_task(const std::vector<std::string>& tasks,
const std::string& query,
std::size_t& out_index) {
for (std::size_t i = 0; i < tasks.size(); ++i) {
if (tasks[i].find(query) != std::string::npos) {
out_index = i;
return true;
}
}
return false;
}
#include <iostream>
#include <string>
#include <vector>
int main() {
std::vector<std::string> tasks{"buy milk", "learn c++", "sleep"};
std::size_t idx = 0;
if (find_task(tasks, "c++", idx)) {
std::cout << idx << '\n'; // 1
}
}
#include <iostream>
#include <string>
#include <vector>
void print_tasks_wrong(std::vector<std::string>& tasks) {
tasks.push_back("oops"); // modificato inaspettatamente il vettore
std::cout << tasks.size() << '\n';
}
#include <iostream>
#include <string>
#include <vector>
void print_tasks(const std::vector<std::string>& tasks);
void add_task(std::vector<std::string>& tasks, const std::string& text);
int main() {
std::vector<std::string> tasks;
add_task(tasks, "learn references");
add_task(tasks, "drink tea");
print_tasks(tasks);
return 0;
}
// Tasks: 2
// 0: learn references
// 1: drink tea
Questo è il problema più comune, perché sembra che «il riferimento sia più veloce» e «sia più giusto». Alla fine ottieni una funzione che per contratto può modificare l'argomento, e il lettore del codice è costretto a sospettare un tranello. Se non modifichi l'oggetto — scegli const T& (per tipi grandi) o il passaggio per valore (per tipi piccoli).
Per esempio, f(std::string("hi")) con f(std::string&) non compila. Non è un capriccio del linguaggio, ma una protezione contro il legare un riferimento a un oggetto che scomparirà «in un attimo». Se vuoi accettare temporanei e solo leggere — usa const T&.
const in const T& è un contratto. Non serve solo al lettore, ma anche al compilatore: vieta modifiche accidentali dell'argomento. Se rimuovi const, disattivi letteralmente la cintura di sicurezza, perché «così è più comodo raggiungere il cassetto».
Il parametro const T x crea comunque una copia — solo che è «immutabile». A volte è utile, ma se l'obiettivo era non copiare std::string/std::vector, allora serve proprio const T&.
Se una funzione modifica un argomento tramite T&, dovrebbe essere evidente dalla firma (lo si vede già), ma è preferibile che anche il nome lo segnali. Altrimenti il prossimo lettore del codice (incluso il te futuro) lancerà il programma «per sicurezza» e controllerà se non si nasconde un altro sgradito effetto collaterale.
GO TO FULL VERSION