Valori di Ritorno delle Funzioni e istruzione return in C++

Concetti Chiave
  • Le funzioni possono restituire valori ai loro chiamanti.
  • L'istruzione return termina l'esecuzione di una funzione e restituisce il controllo al chiamante.
  • Una funzione con un tipo di ritorno diverso da void deve restituire un valore.
  • Una funzione che restituisce un riferimento deve restituire un riferimento a un oggetto preesistente.
  • Non restituire mai un riferimento o un puntatore a un oggetto locale.
  • Una chiamata a una funzione che restituisce un riferimento è un l-value.
  • Per definire una funzione che restituisce un array, usare un alias di tipo o decltype.
  • Per definire una funzione che restituisce un puntatore a un array, usare le parentesi attorno al nome della funzione.
  • Per semplificare la sintassi di ritorno, usare auto e lasciare che il compilatore deduca il tipo di ritorno.

Un'istruzione return termina la funzione che è attualmente in esecuzione e restituisce il controllo al punto da cui la funzione è stata chiamata.

Esistono due forme di istruzioni return. Una senza valore, che ha la forma return; ed una con un valore, che ha la forma return espressione;, dove espressione è qualsiasi espressione valida in C++. In questa lezione, vedremo come e quando usare ciascuna forma di return.

Funzioni senza Valore di Ritorno

Un return senza valore può essere utilizzato solo in una funzione che ha un tipo di ritorno void. Le funzioni che restituiscono void non sono obbligate a contenere un return. In una funzione void, un return implicito avviene dopo l'ultima istruzione della funzione.

Tipicamente, le funzioni void utilizzano un return per uscire dalla funzione in un punto intermedio. Questo uso del return è analogo all'uso di un'istruzione break per uscire da un ciclo. Ad esempio, possiamo scrivere una funzione scambia che non fa nessun lavoro se i valori sono identici:

void scambia(int &v1, int &v2)
{
     // se i valori sono già gli stessi,
     // non c'è bisogno di scambiare,
     // semplicemente ritorna
     if (v1 == v2)
          return;
     // se siamo qui, c'è lavoro da fare
     int tmp = v2;
     v2 = v1;
     v1 = tmp;
     // nessun return esplicito necessario
}

Questa funzione prima verifica se i valori sono uguali e, se è così, esce dalla funzione. Se i valori sono diversi, la funzione li scambia. Un return implicito avviene dopo l'ultima istruzione di assegnazione.

Una funzione con un tipo di ritorno void può utilizzare la seconda forma dell'istruzione return solo per restituire il risultato di una chiamata a un'altra funzione che restituisce void. Restituire qualsiasi altra espressione da una funzione void è un errore di compilazione.

Funzioni che Restituiscono un Valore

La seconda forma dell'istruzione return fornisce il risultato della funzione. Ogni return in una funzione con un tipo di ritorno diverso da void deve restituire un valore. Il valore restituito deve avere lo stesso tipo del tipo di ritorno della funzione, oppure deve avere un tipo che può essere convertito implicitamente a quel tipo.

Anche se C++ non può garantire la correttezza di un risultato, può garantire che ogni return includa un risultato del tipo appropriato. Anche se non può farlo in tutti i casi, il compilatore tenta di assicurare che le funzioni che restituiscono un valore vengano uscite solo attraverso un'istruzione return valida. Ad esempio:

// valori di ritorno scorretti, questo codice non compilerà
bool str_sottointervallo(const string &str1, const string &str2)
{
    // stesse dimensioni: restituisci un test di uguaglianza normale
    if (str1.size() == str2.size())
        return str1 == str2; // ok: == restituisce bool
    // trova la dimensione della stringa più piccola;
    // operatore condizionale
    auto dimensione = (str1.size() < str2.size())
                    ? str1.size() : str2.size();

    // guarda ogni elemento fino alla dimensione della stringa più piccola
    for (decltype(dimensione) i = 0; i != dimensione; ++i) {
        if (str1[i] != str2[i])
            // ERRORE #1: nessun valore di ritorno;
            // il compilatore dovrebbe rilevare questo errore
            return;
     }
     // ERRORE #2: il controllo potrebbe uscire dalla fine della funzione senza un return
     // il compilatore potrebbe non rilevare questo errore
}

Il return dall'interno del ciclo for è un errore perché non riesce a restituire un valore. Il compilatore dovrebbe rilevare questo errore.

Il secondo errore si verifica perché la funzione non riesce a fornire un return dopo il ciclo. Se chiamassimo questa funzione con una stringa che è un sottoinsieme dell'altra, l'esecuzione uscirebbe dal for. Dovrebbe esserci un return per gestire questo caso. Il compilatore può o non può rilevare questo errore. Se non rileva l'errore, quello che succede a runtime è indefinito.

Non riuscire a fornire un return dopo un ciclo che contiene un return è un errore. Tuttavia, molti compilatori non rileveranno tali errori.

Come i Valori vengono Restituiti

I valori vengono restituiti esattamente nello stesso modo in cui le variabili e i parametri vengono inizializzati: Il valore di ritorno viene utilizzato per inizializzare un temporaneo nel sito di chiamata, e quel temporaneo è il risultato della chiamata di funzione.

È importante tenere a mente le regole di inizializzazione nelle funzioni che restituiscono variabili locali. Come esempio, potremmo scrivere una funzione che, dato un contatore, una parola e una desinenza, ci restituisce la versione plurale della parola se il contatore è maggiore di 1:

// restituisce la versione plurale di parola se ctr è maggiore di 1
string crea_plurale(size_t ctr, const string &parola,
                                const string &desinenza)
{
    return (ctr > 1) ? parola + desinenza : parola;
}

Il tipo di ritorno di questa funzione è string, che significa che il valore di ritorno viene copiato al sito di chiamata. Questa funzione restituisce una copia di parola, oppure restituisce una string temporanea senza nome che risulta dall'aggiunta di parola e desinenza.

Come con qualsiasi altro riferimento, quando una funzione restituisce un riferimento, quel riferimento è solo un altro nome per l'oggetto a cui si riferisce. Come esempio, consideriamo una funzione che restituisce un riferimento alla più corta dei suoi due parametri string:

// restituisce un riferimento alla più corta di due stringhe
const string &stringaPiuCorta(const string &s1, const string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}

I parametri e il tipo di ritorno sono riferimenti a const string. Le stringhe non vengono copiate quando la funzione viene chiamata o quando il risultato viene restituito.

Non Restituire mai un Riferimento o un Puntatore a un Oggetto Locale

Quando una funzione si completa, il suo storage viene liberato. Dopo che una funzione termina, i riferimenti agli oggetti locali si riferiscono a memoria che non è più valida:

// disastro: questa funzione restituisce un riferimento a un oggetto locale
const string &manipola()
{
    string ret;
    // trasforma ret in qualche modo
    if (!ret.empty())
        return ret; // SBAGLIATO: restituisce un riferimento a un oggetto locale!
    else
        return "Vuoto"; // SBAGLIATO: "Vuoto" è una string temporanea locale
}

Entrambe queste istruzioni return restituiscono un valore indefinito. Quello che succede se proviamo ad utilizzare il valore restituito da manipola è indefinito. Nel primo return, dovrebbe essere ovvio che la funzione restituisce un riferimento a un oggetto locale. Nel secondo caso, il letterale stringa viene convertito in un oggetto string temporaneo locale. Quell'oggetto, come la string chiamata ret, è locale alla funzione manipola. Lo storage in cui risiede il temporaneo viene liberato quando la funzione finisce. Entrambi i return si riferiscono a memoria che non è più disponibile.

Un buon modo per assicurarsi che il return sia sicuro è chiedere: A quale oggetto preesistente si riferisce il riferimento?

Per le stesse ragioni per cui è sbagliato restituire un riferimento a un oggetto locale, è anche sbagliato restituire un puntatore a un oggetto locale. Una volta che la funzione si completa, gli oggetti locali vengono liberati. Il puntatore punterebbe a un oggetto inesistente.

Funzioni che Restituiscono Tipi di Classe e l'Operatore di Chiamata

Come qualsiasi operatore, l'operatore di chiamata ha associatività e precedenza. L'operatore di chiamata ha la stessa precedenza degli operatori punto e freccia. Come quegli operatori, l'operatore di chiamata è associativo a sinistra. Di conseguenza, se una funzione restituisce un puntatore, riferimento o oggetto di tipo classe, possiamo utilizzare il risultato di una chiamata per chiamare un membro dell'oggetto risultante.

Ad esempio, possiamo determinare la dimensione della stringa più corta come segue:

// chiama il membro size della string restituita da stringaPiuCorta
auto dim = stringaPiuCorta(s1, s2).size();

Poiché questi operatori sono associativi a sinistra, il risultato di stringaPiuCorta è l'operando sinistro dell'operatore punto. Quell'operatore recupera il membro size di quella stringa. Quel membro è l'operando sinistro del secondo operatore di chiamata.

I Return di Riferimento sono L-Value

Se una chiamata di funzione è un l-value dipende dal tipo di ritorno della funzione. Le chiamate a funzioni che restituiscono riferimenti sono l-value; altri tipi di ritorno producono r-value. Una chiamata a una funzione che restituisce un riferimento può essere utilizzata negli stessi modi di qualsiasi altro l-value. In particolare, possiamo assegnare al risultato di una funzione che restituisce un riferimento a non const:

char &ottieni_val(string &str, string::size_type ix)
{
    return str[ix]; // ottieni_val assume che l'indice dato sia valido
}

int main()
{
    string s("un valore");
    cout << s << endl; // stampa un valore
    ottieni_val(s, 0) = 'U'; // cambia s[0] in U
    cout << s << endl; // stampa Un valore
    return 0;
}

Può essere sorprendente vedere una chiamata di funzione sul lato sinistro di un'assegnazione. Tuttavia, niente di speciale è coinvolto. Il valore di ritorno è un riferimento, quindi la chiamata è un l-value. Come qualsiasi altro l-value, può apparire come operando sinistro dell'operatore di assegnazione.

Se il tipo di ritorno è un riferimento a const, allora (come al solito) non possiamo assegnare al risultato della chiamata:

stringaPiuCorta("ciao", "addio") = "X"; // ERRORE: valore di ritorno è const

Inizializzazione con Lista del Valore di Ritorno

A partire da C++11, le funzioni possono restituire una lista di valori tra parentesi graffe. Come in qualsiasi altro return, la lista viene utilizzata per inizializzare il temporaneo che rappresenta il ritorno della funzione. Se la lista è vuota, quel temporaneo viene inizializzato al valore. Altrimenti, il valore del return dipende dal tipo di ritorno della funzione.

Come esempio, ricordiamo la funzione msg_errore vista in precedenza:

void msg_errore(initializer_list<string> il)
{
    for (auto inizio = il.begin(); inizio != il.end(); ++inizio)
        cout << *inizio << " " ;
    cout << endl;
}

Quella funzione prendeva un numero variabile di argomenti string e stampava un messaggio di errore composto dalle stringhe date. Anziché chiamare msg_errore, in questa funzione restituiremo un vector<string> che contiene le stringhe del messaggio di errore:

vector<string> elabora()
{
    // ...
    // atteso e effettivo sono stringhe
    if (atteso.empty())
        return {}; // restituisce un vector vuoto
    else if (atteso == effettivo)
        return {"funzioneX", "ok"}; // restituisce vector inizializzato con lista
    else
        return {"funzioneX", atteso, effettivo};
}

Nella prima istruzione return, restituiamo una lista vuota. In questo caso, il vector che elabora restituisce sarà vuoto. Altrimenti, restituiamo un vector inizializzato con due o tre elementi a seconda che atteso ed effettivo siano uguali.

In una funzione che restituisce un tipo built-in, una lista con parentesi graffe può contenere al massimo un valore, e quel valore non deve richiedere una conversione di restringimento. Se la funzione restituisce un tipo classe, allora la classe stessa definisce come vengono utilizzati gli inizializzatori.

return in main

C'è una eccezione alla regola che una funzione con un tipo di ritorno diverso da void deve restituire un valore: La funzione main è autorizzata a terminare senza un return. Se il controllo raggiunge la fine di main e non c'è return, allora il compilatore inserisce implicitamente un return di 0.

Come abbiamo visto in precedenza, il valore restituito da main viene trattato come un indicatore di stato. Un return zero indica successo; la maggior parte degli altri valori indicano fallimento. Un valore non zero ha un significato dipendente dalla macchina. Per rendere i valori di ritorno indipendenti dalla macchina, l'header cstdlib definisce due variabili preprocessore che possiamo utilizzare per indicare successo o fallimento:

#include <cstdlib> // per EXIT_SUCCESS e EXIT_FAILURE

int main()
{
    if (qualche_fallimento)
        return EXIT_FAILURE; // definito in cstdlib
    else
        return EXIT_SUCCESS; // definito in cstdlib
}

Poiché queste sono variabili preprocessore, non dobbiamo precederle con std::, né possiamo menzionarle nelle dichiarazioni using.

Ricorsione

Una funzione che chiama se stessa, sia direttamente che indirettamente, è una funzione ricorsiva. Come esempio, possiamo riscrivere la nostra funzione fattoriale per utilizzare la ricorsione:

// calcola val!, che è 1 * 2 * 3 ... * val
int fattoriale(int val)
{
    if (val > 1)
        return fattoriale(val-1) * val;
    return 1;
}

In questa implementazione, chiamiamo ricorsivamente fattoriale per calcolare il fattoriale dei numeri contando verso il basso dal valore originale in val. Una volta che abbiamo ridotto val a 1, fermiamo la ricorsione restituendo 1.

Deve sempre esserci un percorso attraverso una funzione ricorsiva che non coinvolga una chiamata ricorsiva; altrimenti, la funzione ricorrerà "per sempre," significando che la funzione continuerà a chiamare se stessa fino a quando lo stack del programma è esaurito. Tali funzioni sono talvolta descritte come contenenti un ciclo di ricorsione. Nel caso di fattoriale, la condizione di arresto si verifica quando val è 1.

La seguente tabella traccia l'esecuzione di fattoriale quando viene passato il valore 5.

Chiamata Restituisce Valore
fattoriale(5) fattoriale(4) * 5 120
fattoriale(4) fattoriale(3) * 4 24
fattoriale(3) fattoriale(2) * 3 6
fattoriale(2) fattoriale(1) * 2 2
fattoriale(1) 1 1
Tabella 1: Stack di chiamate per fattoriale(5)

Esiste un'eccezione però: La funzione main non può chiamare se stessa.

Esercizi

  • Quando è valido restituire un riferimento? Un riferimento a const?

    Risposta: È valido restituire un riferimento quando il riferimento si riferisce a un oggetto che esiste ancora dopo che la funzione si è completata. Un riferimento a const può essere restituito in qualsiasi momento, ma non possiamo modificare l'oggetto a cui si riferisce il riferimento.

  • Indica se la seguente funzione è legale. Se sì, spiega cosa fa; se no, correggi eventuali errori e poi spiegala.

    int &ottieni(int *array, int indice) { return array[indice]; }
    int main() {
        int ia[10];
        for (int i = 0; i != 10; ++i)
            ottieni(ia, i) = i;
    }
    

    Risposta: La funzione è legale. Restituisce un riferimento all'elemento dell'array specificato dall'indice. Nel main, la funzione viene utilizzata per assegnare i valori da 0 a 9 agli elementi dell'array ia.

  • Scrivi una funzione ricorsiva per stampare il contenuto di un vector.

    Risposta:

    void stampaVector(const vector<int>& vec, size_t index = 0) {
        if (index < vec.size()) {
            cout << vec[index] << " ";
            stampaVector(vec, index + 1);
        }
    }
    
  • Cosa succederebbe se la condizione di arresto in fattoriale fosse if (val != 0)?

    Risposta: La funzione continuerebbe a chiamare se stessa fino a quando val diventa pari a zero. Tuttavia, così facendo, il risultato sarebbe errato perché il fattoriale di 0 è definito come 1, e la funzione restituirebbe 0! * 1 = 0, che è sbagliato.

  • Nella chiamata a fattoriale, perché abbiamo passato val - 1 anziché val--?

    Risposta: Abbiamo passato val - 1 perché val-- decrementebbe val dopo che il valore è stato passato alla funzione, il che non è l'intento. Vogliamo passare il valore decrementato immediatamente alla chiamata ricorsiva.

Restituire un Puntatore a un Array

Poiché non possiamo copiare un array, una funzione non può restituire un array. Tuttavia, una funzione può restituire un puntatore o un riferimento a un array. Sfortunatamente, la sintassi utilizzata per definire funzioni che restituiscono puntatori o riferimenti a array può essere intimidatoria. Fortunatamente, ci sono modi per semplificare tali dichiarazioni. Il modo più diretto è utilizzare un alias di tipo:

// arrayT è un sinonimo per il tipo array di dieci int
typedef int arrayT[10];

// dichiarazione equivalente di arrayT
using arrayT = int[10];

// func restituisce un puntatore a un array di dieci int
arrayT* func(int i);

Qui arrayT è un sinonimo per un array di dieci int. Poiché non possiamo restituire un array, definiamo il tipo di ritorno come un puntatore a questo tipo. Quindi, func è una funzione che prende un singolo argomento int e restituisce un puntatore a un array di dieci int.

Dichiarare una Funzione che Restituisce un Puntatore a un Array

Per dichiarare func senza utilizzare un alias di tipo, dobbiamo ricordare che la dimensione di un array segue il nome che viene definito:

int array[10]; // array è un array di dieci int
int *p1[10]; // p1 è un array di dieci puntatori
int (*p2)[10] = &array; // p2 punta a un array di dieci int

Come con queste dichiarazioni, se vogliamo definire una funzione che restituisce un puntatore a un array, la dimensione deve seguire il nome della funzione. Tuttavia, una funzione include una lista di parametri, che segue anche il nome. La lista di parametri precede la dimensione. Quindi, la forma di una funzione che restituisce un puntatore a un array è:

Tipo (*funzione(lista_parametri))[dimensione]

Come in qualsiasi altra dichiarazione di array, Tipo è il tipo degli elementi e dimensione è la dimensione dell'array. Le parentesi attorno a (*funzione(lista_parametri)) sono necessarie per la stessa ragione per cui erano richieste quando abbiamo definito p2. Senza di esse, staremmo definendo una funzione che restituisce un array di puntatori.

Come esempio concreto, il seguente dichiara func senza utilizzare un alias di tipo:

int (*func(int i))[10];

Per capire questa dichiarazione, può essere utile pensarci come segue:

  • func(int) dice che possiamo chiamare func con un argomento int.
  • (*func(int)) dice che possiamo dereferenziare il risultato di quella chiamata.
  • (*func(int))[10] dice che dereferenziare il risultato di una chiamata a func produce un array di dimensione dieci.
  • int (*func(int))[10] dice che il tipo dell'elemento in quell'array è int.

Utilizzare un Tipo di Ritorno Trailing

A partire da C++11, un altro modo per semplificare la dichiarazione di func è utilizzando un tipo di ritorno Trailing. I ritorni trailing possono essere definiti per qualsiasi funzione, ma sono più utili per funzioni con tipi di ritorno complicati, come puntatori (o riferimenti) ad array. Un tipo di ritorno trailing segue la lista di parametri ed è preceduto da ->. Per segnalare che il return segue la lista di parametri, utilizziamo auto dove ordinariamente appare il tipo di ritorno:

// fcn prende un argomento int e restituisce un puntatore a un array di dieci int
auto func(int i) -> int(*)[10];

Poiché il tipo di ritorno viene dopo la lista di parametri, è più facile vedere che func restituisce un puntatore e che quel puntatore punta a un array di dieci int.

Utilizzare decltype

Come altra alternativa, se conosciamo l'array/gli array a cui la nostra funzione può restituire un puntatore, possiamo utilizzare decltype per dichiarare il tipo di ritorno. Ad esempio, la seguente funzione restituisce un puntatore a uno di due array, a seconda del valore del suo parametro:

int dispari[] = {1,3,5,7,9};
int pari[] = {0,2,4,6,8};
// restituisce un puntatore a un array di cinque elementi int
decltype(dispari) *ptrArray(int i)
{
    // restituisce un puntatore all'array
    return (i % 2) ? &dispari : &pari;
}

Il tipo di ritorno per ptrArray utilizza decltype per dire che la funzione restituisce un puntatore a qualsiasi tipo abbia dispari. Quell'oggetto è un array, quindi ptrArray restituisce un puntatore a un array di cinque int. L'unica parte difficile è che dobbiamo ricordare che decltype non converte automaticamente un array al suo tipo di puntatore corrispondente. Il tipo restituito da decltype è un tipo array, al quale dobbiamo aggiungere un asterisco * per indicare che ptrArray restituisce un puntatore.

Esercizi

  • Scrivi la dichiarazione per una funzione che restituisce un riferimento a un array di dieci stringhe, senza utilizzare né un ritorno trailing, decltype, o un alias di tipo.

    Risposta:

    string (&func())[10];
    
  • Scrivi tre dichiarazioni aggiuntive per la funzione nell'esercizio precedente. Una dovrebbe utilizzare un alias di tipo, una dovrebbe utilizzare un ritorno trailing, e la terza dovrebbe utilizzare decltype.

    Risposta:

    Utilizzando un alias di tipo:

    using arrayString = string[10];
    arrayString& func();
    

    Utilizzando un ritorno trailing:

    auto func() -> string(&)[10];
    

    Utilizzando decltype (assumendo che esista un array di dieci stringhe chiamato arr):

    string arr[10];
    decltype(arr)& func();
    
  • Rivedi la funzione ptrArray per restituire un riferimento all'array.

    Risposta:

    int dispari[] = {1,3,5,7,9};
    int pari[] = {0,2,4,6,8};
    // restituisce un riferimento a un array di cinque elementi int
    decltype(dispari)& ptrArray(int i)
    {
        // restituisce un riferimento all'array
        return (i % 2) ? dispari : pari;
    }