HOWTO · C++

Operatori pre-incremento VS post-incremento in C++

Questo articolo mostra come utilizzare il pre-incremento VS il post-incremento in C++

In questa pagina

Per una variabile aritmetica integrata in C++, sia ++x sia x++ aggiungono uno a x. La differenza sta nel valore prodotto dall’espressione: il pre-incremento produce il valore aggiornato, mentre il post-incremento produce il valore precedente all’aggiornamento. Scegli in base al valore richiesto dall’espressione circostante. Per un tipo definito dall’utente, gli operatori di incremento sovraccaricati seguono le proprie definizioni; per convenzione, di solito offrono comportamenti analoghi per prefisso e postfisso.

Differenza tra pre-incremento e post-incremento in C++

Per i tipi aritmetici integrati, ++x aggiorna x prima di produrre il risultato dell’espressione. x++ produce il valore precedente e completa l’incremento nell’ambito dell’operazione. In entrambi i casi, la variabile cambia una sola volta. Gli operatori sovraccaricati possono definire dettagli diversi, anche se per convenzione seguono la stessa distinzione tra prefisso e postfisso.

Esegui questo esempio:

#include <iostream>

int main() {
    int value = 4;
    int previous = value++;
    int current = ++value;

    std::cout << previous << ' ' << current << ' ' << value << '\n';
}
4 6 6

previous riceve il valore precedente al primo incremento; current riceve il valore successivo al secondo incremento, che coincide anche con il valore finale di value.

La posizione dell’incremento nel corpo di un ciclo cambia il valore stampato dal corpo. Nel primo ciclo, i++ è l’espressione di stampa: stampa il valore corrente e poi incrementa i. Nel secondo, ++j viene eseguito prima dell’espressione di stampa, quindi il corpo stampa il valore aggiornato.

#include <iostream>

int main() {
    int i = 0;
    while (i < 5) {
        std::cout << i++ << '\n';
    }
    std::cout << "final i = " << i << '\n';
}
#include <iostream>

int main() {
    int j = 0;
    while (j < 5) {
        std::cout << ++j << '\n';
    }
    std::cout << "final j = " << j << '\n';
}

Il primo ciclo stampa 0, 1, 2, 3 e 4, seguiti da final i = 5; il secondo stampa 1, 2, 3, 4 e 5, seguiti da final j = 5. In entrambi i casi il contatore parte da 0, il corpo viene eseguito cinque volte e il valore finale del contatore è 5. Questi esempi incrementano la variabile nel corpo del ciclo. Invece, quando l’incremento si trova nell’espressione di iterazione di un ciclo for, viene eseguito dopo il corpo. Se il risultato viene ignorato, sostituire ++k con k++ lascia invariata la sequenza: sia for (int k = 0; k < 5; ++k) sia la versione con k++ visitano 0, 1, 2, 3 e 4, per poi terminare con k pari a 5. Le etichette final i e final j restano in inglese perché il codice e l’output dell’esempio sono mantenuti invariati.

Evitare di modificare e leggere la stessa variabile in un’unica espressione

Non combinare l’incremento di una variabile con un’altra lettura della stessa variabile in un’espressione in cui le operazioni non sono sequenziate. Per esempio, k++ * k ha comportamento indefinito in C++: la modifica e l’altra lettura non sono sequenziate l’una rispetto all’altra. Il risultato non è prevedibile e non può essere usato come output valido di un esempio.

Se vuoi moltiplicare il valore precedente per quello successivo, rendi espliciti e sequenziali i passaggi: inizializza k, salvalo in old, incrementa k e poi calcola old * k.

Le istruzioni separate salvano il valore precedente prima di incrementare k, quindi moltiplicano il valore salvato per quello aggiornato. Esegui questo esempio:

#include <iostream>

int main() {
    int k = 10;
    int old = k;
    ++k;
    int product = old * k;

    std::cout << product << '\n';
}
110

Come cambia la scelta con iteratori e tipi definiti dall’utente

La distinzione tra i valori conta soprattutto quando un incremento è combinato con un’altra operazione nella stessa espressione. Il caso comune è la dereferenziazione: *it++ significa *(it++), quindi legge l’elemento a cui l’iteratore punta in quel momento e poi fa avanzare l’iteratore. Al contrario, *++it avanza prima e poi legge il nuovo elemento. Saltare o ripetere un elemento durante una scansione è spesso proprio questa confusione tra prefisso e postfisso, e non un errore nella condizione del ciclo.

#include <iostream>
#include <vector>

int main() {
    std::vector<int> values{10, 20, 30};
    auto it = values.begin();

    std::cout << *it++ << '\n';
    std::cout << *it << '\n';
    std::cout << *++it << '\n';
}
10
20
30

La prima riga stampa 10 e lascia l’iteratore a puntare a 20; stampare *it conferma la nuova posizione prima che *++it avanzi di nuovo e stampi 30.

Per un tipo definito dall’utente, le due forme sono due funzioni diverse. Per convenzione, il pre-incremento è dichiarato come operator++() e restituisce un riferimento all’oggetto aggiornato, mentre il post-incremento è dichiarato come operator++(int) — quell’int è un marcatore fittizio, non un valore passato dal chiamante —, salva una copia dello stato precedente, incrementa tramite la forma prefissa e restituisce quella copia per valore.

#include <iostream>

class Counter {
public:
    explicit Counter(int value) : value_(value) {}

    Counter& operator++() {
        ++value_;
        return *this;
    }

    Counter operator++(int) {
        Counter old = *this;
        ++(*this);
        return old;
    }

    int value() const { return value_; }

private:
    int value_;
};

int main() {
    Counter counter{5};
    Counter old = counter++;
    Counter& ref = ++counter;

    std::cout << old.value() << ' ' << counter.value() << ' '
              << ( &ref == &counter ) << '\n';
}
5 7 1

Qui old conserva il valore iniziale 5, il contatore stesso termina a 7 dopo entrambi gli incrementi, e l’1 finale conferma che il pre-incremento ha restituito il contatore stesso e non una copia. La stessa distinzione spiega perché il risultato di ++x si riferisce ancora a x stesso, mentre il risultato di x++ è una copia temporanea del vecchio valore.

È anche qui che la questione delle prestazioni diventa concreta. Per un intero integrato il cui risultato viene scartato, i compilatori generano in genere lo stesso codice per entrambe le forme, quindi preferire il prefisso in quel caso è una convenzione di leggibilità, non un aumento di velocità. La copia del post-incremento costa qualcosa solo quando copiare è un lavoro reale: un iteratore o un contatore definito dall’utente che copia lo stato, o un operatore sovraccaricato che fa più che aggiungere uno. Se un tipo definisce solo una delle due forme, l’altra semplicemente non è disponibile; controllate le dichiarazioni del tipo invece di presupporre che esistano entrambe.

Quando usare il pre-incremento o il post-incremento

Usa il post-incremento quando l’espressione ha bisogno del valore precedente, come in int previous = value++;. Usa il pre-incremento quando serve il valore aggiornato oppure quando il valore dell’espressione viene ignorato, come nell’espressione di iterazione di un ciclo.

Per gli iteratori, il pre-incremento è una convenzione comune quando non serve il valore precedente dell’iteratore. Gli operatori sovraccaricati di un iteratore definito dall’utente seguono la relativa implementazione, anche se per convenzione rispecchiano la distinzione integrata tra prefisso e postfisso. Un’operazione postfissa su un iteratore può conservare e restituire una copia dell’iteratore precedente; per i tipi di iteratore in cui la copia ha un costo, il pre-incremento può evitare quel lavoro. Questo non dimostra che un programma specifico sia misurabilmente più veloce: il lavoro generato dipende dall’ottimizzazione del compilatore e dall’implementazione dell’iteratore.

Conclusione

Entrambe le forme incrementano la variabile una volta. Il pre-incremento restituisce il valore aggiornato, mentre il post-incremento restituisce quello precedente. Scegli la forma che comunica chiaramente quale valore serve all’espressione; per gli iteratori, preferisci il pre-incremento quando il valore precedente non viene usato, senza presumere che garantisca un aumento della velocità.

Domande frequenti

Qual è la differenza principale tra pre-incremento e post-incremento?

Per i tipi aritmetici integrati, ++x aggiorna la variabile prima di restituire il valore aggiornato. x++ restituisce il valore precedente e completa l’incremento. Gli operatori sovraccaricati seguono le proprie definizioni, anche se per convenzione offrono comportamenti analoghi per prefisso e postfisso.

Posso usare indifferentemente entrambe le forme in un ciclo?

Sì. Nell’espressione di iterazione di un ciclo for, il risultato viene di solito ignorato; quindi sostituire ++i con i++ lascia invariata la sequenza di iterazione del ciclo.

Quale forma è più veloce in C++?

Non esiste una classifica di velocità valida per ogni espressione e tipo. Il post-incremento può comportare una copia temporanea per gli iteratori o per i tipi definiti dall’utente; l’ottimizzazione può eliminare il lavoro non necessario. Scegli in base al valore necessario e alla chiarezza dell’intento, e misura un carico di lavoro reale prima di fare affermazioni sulle prestazioni.

Quando è utile il post-incremento?

Usalo quando l’espressione ha bisogno del valore precedente all’incremento, per esempio per assegnarlo a un’altra variabile.

Pre-incremento e post-incremento si comportano allo stesso modo in ogni linguaggio?

No. Operatori scritti in modo simile possono seguire regole specifiche del linguaggio. Verifica la semantica del linguaggio che usi: le spiegazioni qui riguardano C++.