Operatori di Incremento e Decremento in C++

Concetti Chiave
  • Gli operatori di incremento (++) e decremento (--) forniscono una comoda abbreviazione notazionale per aggiungere o sottrarre 1 da un oggetto.
  • Ci sono due forme di questi operatori: prefissa e postfissa.
  • Gli operatori prefissi incrementano (o decrementano) il loro operando e producono l'oggetto modificato come risultato.
  • Gli operatori postfissi incrementano (o decrementano) l'operando ma producono una copia del valore originale, non modificato come risultato.
  • Questi operatori richiedono operandi l-value.
  • Gli operatori prefissi restituiscono l'oggetto stesso come l-value.
  • Gli operatori postfissi restituiscono una copia del valore originale dell'oggetto come r-value.
  • Le versioni postfisse di ++ e -- vengono usate quando vogliamo usare il valore corrente di una variabile e incrementarla in una singola espressione composta.

Operatori di Incremento e Decremento

Gli operatori di incremento (++) e decremento (--) forniscono una comoda abbreviazione notazionale per aggiungere o sottrarre 1 da un oggetto. Questa notazione va oltre la mera convenienza quando usiamo questi operatori con gli iteratori, perché molti iteratori non supportano l'aritmetica.

Ci sono due forme di questi operatori: prefissa e postfissa. Finora, abbiamo usato solo la forma prefissa. Questa forma incrementa (o decrementa) il suo operando e produce l'oggetto modificato come risultato. Gli operatori postfissi incrementano (o decrementano) l'operando ma producono una copia del valore originale, non modificato come risultato:

int i = 0, j;

// j = 1, i = 1: il prefisso produce il valore incrementato
j = ++i;

// j = 1, i = 2: il postfisso produce il valore non incrementato
j = i++;

Questi operatori richiedono operandi l-value. Gli operatori prefissi restituiscono l'oggetto stesso come l-value. Gli operatori postfissi restituiscono una copia del valore originale dell'oggetto come r-value.

Consiglio

Usare gli operatori postfissi solo quando necessario

I lettori con un background in linguaggio C potrebbero essere sorpresi che usiamo l'incremento prefisso nei programmi che abbiamo scritto. La ragione è semplice: La versione prefissa evita lavoro non necessario. Incrementa il valore e restituisce la versione incrementata. L'operatore postfisso deve memorizzare il valore originale in modo da poter restituire il valore non incrementato come risultato. Se non abbiamo bisogno del valore non incrementato, non c'è bisogno del lavoro extra fatto dall'operatore postfisso.

Per int e puntatori, il compilatore può ottimizzare via questo lavoro extra. Per tipi iteratori più complicati, questo lavoro extra potrebbe potenzialmente essere più costoso. Usando abitualmente le versioni prefisse, non dobbiamo preoccuparci se la differenza di prestazioni conta. Inoltre possiamo esprimere l'intento dei nostri programmi in maniera più diretta.

Combinare Dereferenziazione e Incremento in una Singola Espressione

Le versioni postfisse di ++ e -- vengono usate quando vogliamo usare il valore corrente di una variabile e incrementarla in una singola espressione composta.

Come esempio, possiamo usare l'incremento postfisso per scrivere un ciclo per stampare i valori in un vector fino a, ma non includendo, il primo valore negativo:

auto pbeg = v.begin();
// stampa gli elementi fino al primo valore negativo
while (pbeg != v.end() && *pbeg >= 0)
    // stampa il valore corrente e avanza pbeg
    cout << *pbeg++ << endl;

L'espressione *pbeg++ solitamente crea confusione per i programmatori nuovi sia a C++ che a C. Tuttavia, poiché questo pattern di utilizzo è così comune, i programmatori C++ devono comprendere tali espressioni.

La precedenza dell'incremento postfisso è più alta di quella dell'operatore di dereferenziazione, quindi *pbeg++ è equivalente a *(pbeg++). La sottoespressione pbeg++ incrementa pbeg e produce una copia del valore precedente di pbeg come risultato. Di conseguenza, l'operando di * è il valore non incrementato di pbeg. Quindi, l'istruzione stampa l'elemento a cui pbeg puntava originariamente e incrementa pbeg.

Questo utilizzo si basa sul fatto che l'incremento postfisso restituisce una copia del suo operando originale, non incrementato. Se restituisse il valore incrementato, dereferenzieremmo il valore incrementato, con risultati disastrosi. Salteremmo il primo elemento. Peggio, se la sequenza non avesse valori negativi, tenteremmo di dereferenziare un elemento di troppo.

Consiglio

C++ idiomatico e brevità

Espressioni come *pbeg++ possono essere sconcertanti all'inizio. Tuttavia, è un idioma utile e ampiamente usato. Una volta che la notazione è familiare, scrivere

cout << *iter++ << endl;

è più facile e meno soggetto a errori dell'equivalente più verboso

cout << *iter << endl;
++iter;

Vale la pena studiare esempi di tale codice finché i loro significati non sono immediatamente chiari. La maggior parte dei programmi C++ usa espressioni succinte piuttosto che equivalenti più verbosi. Pertanto, i programmatori C++ devono essere a proprio agio con tali utilizzi. Inoltre, una volta che queste espressioni sono familiari, le troverai meno soggette a errori.

Gli Operandi Possono Essere Valutati in Qualsiasi Ordine

La maggior parte degli operatori non dà alcuna garanzia sull'ordine in cui gli operandi saranno valutati. Questa mancanza di ordine garantito spesso non conta. I casi in cui conta sono quando una sotto-espressione cambia il valore di un operando che viene usato in un'altra sotto-espressione. Poiché gli operatori di incremento e decremento cambiano i loro operandi, è facile usare male questi operatori in espressioni composte.

Per illustrare il problema, riprendiamo l'esempio usato nella lezione sugli iteratori in cui utilizzavamo un iteratore per capitalizzare la prima parola in una stringa. Quell'esempio usava un ciclo for:

for (auto it = s.begin(); it != s.end() && !isspace(*it); ++it)
    *it = toupper(*it); // capitalizza il carattere corrente

che ci permetteva di separare l'istruzione che dereferenziava beg da quella che la incrementava. Sostituire il for con un apparentemente equivalente while

// il comportamento del seguente ciclo è indefinito!
while (beg != s.end() && !isspace(*beg))
    // ERRORE: questo assegnamento è indefinito
    *beg = toupper(*beg++);

risulta in comportamento indefinito. Il problema è che nella versione rivista, sia gli operandi sinistro che destro di = usano beg e l'operando destro cambia beg. L'assegnamento è quindi indefinito. Il compilatore potrebbe valutare questa espressione come

*beg = toupper(*beg); // esecuzione se il lato sinistro è valutato per primo
*(beg + 1) = toupper(*beg); // esecuzione se il lato destro è valutato per primo

oppure potrebbe valutarla in qualche altro modo ancora.

Esercizi

  • Cosa succederebbe se il ciclo while dell'esempio di sopra che stampa gli elementi da un vector usasse l'operatore di incremento prefisso?

    auto pbeg = v.begin();
    // stampa gli elementi fino al primo valore negativo
    while (pbeg != v.end() && *pbeg >= 0)
        // stampa il valore corrente e avanza pbeg
        cout << *pbeg++ << endl;
    

    Soluzione: Usare l'operatore di incremento prefisso in questo esempio produrrebbe un comportamento errato. L'espressione *pbeg++ verrebbe interpretata come *(++pbeg), il che significa che pbeg verrebbe incrementato prima della dereferenziazione. Di conseguenza, il primo elemento del vector verrebbe saltato e il ciclo inizierebbe a stampare dal secondo elemento. Se il vector non contenesse valori negativi, si tenterebbe di dereferenziare un elemento oltre la fine del vector, portando a un comportamento indefinito.

  • Dato che ptr punta a un int, che vec è un vector<int>, e che ival è un int, spiega il comportamento di ciascuna di queste espressioni. Quali, se ce ne sono, è probabile che siano incorrette? Perché? Come potrebbe essere corretto ciascuno?

    ptr != 0 && *ptr++
    ival++ && ival
    vec[ival++] <= vec[ival]
    

    Soluzione:

    • ptr != 0 && *ptr++: Questa espressione è corretta. Valuta se ptr non è nullo e, se vero, dereferenzia ptr prima di incrementarlo. Non c'è conflitto tra l'uso e la modifica di ptr perché l'incremento avviene dopo la dereferenziazione.
    • ival++ && ival: In questo caso, dato che l'operatore && valuta il suo operando sinistro prima del destro, l'operando destro utilizza il valore di ival dopo che è stato incrementato. Quindi, questa espressione è corretta e non causa conflitti. Gli operatori logici && e || sono gli unici operatori in C++ che garantiscono un ordine di valutazione specifico.
    • vec[ival++] <= vec[ival]: Questa espressione è problematica. Non c'è garanzia sull'ordine di valutazione degli operandi di <=, quindi ival potrebbe essere incrementato prima o dopo che viene usato nel secondo operando. Questo porta a un comportamento indefinito perché ival viene sia letto che modificato senza un ordine definito. Per correggere questa espressione, si potrebbe separare l'incremento in una riga distinta:
    int currentIndex = ival++;
    bool result = vec[currentIndex] <= vec[ival];