CodeGym /Corsi /C++ SELF /T& e const T& — «secondo nome»

T& e const T& — «secondo nome»

C++ SELF
Livello 15, Lezione 1
Disponibile

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:

  1. dici «non voglio copiare, dammi accesso all'oggetto originale»
  2. 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»
T x
viene creata una copia dell'argomento
«Modificherò il tuo oggetto»
T& x
lavoro con l'oggetto originale, le modifiche sono visibili all'esterno
«Leggo solo, non voglio copia»
const T& x
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.

1
Compito
C++ SELF, livello 15, lezione 1
Bloccato
Aggiornare il contatore
Aggiornare il contatore
1
Compito
C++ SELF, livello 15, lezione 1
Bloccato
Contatore di caratteri
Contatore di caratteri
1
Compito
C++ SELF, livello 15, lezione 1
Bloccato
Rimuovere uno spazio
Rimuovere uno spazio
1
Compito
C++ SELF, livello 15, lezione 1
Bloccato
Primo elemento corrispondente
Primo elemento corrispondente
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION