Perché ++ ho considerato un valore-l, ma i ++ no?

Perché ++ i è l-value e i ++ no?

    Beh, come un altro rispondente ha già indicato il motivo per cui ++i è un lvalue è quello di passarlo a un riferimento.

     int v = 0; int const & rcv = ++v; // would work if ++v is an rvalue too int & rv = ++v; // would not work if ++v is an rvalue 

    Il motivo per cui la seconda regola è consentire di inizializzare un riferimento utilizzando un valore letterale, quando il riferimento è un riferimento a const:

     void taking_refc(int const& v); taking_refc(10); // valid, 10 is an rvalue though! 

    Perché introduciamo un valore massimo che potresti chiedere. Bene, questi termini emergono quando si costruiscono le regole linguistiche per queste due situazioni:

    • Vogliamo avere un valore di localizzatore. Quello rappresenterà una posizione che contiene un valore che può essere letto.
    • Vogliamo rappresentare il valore di un’espressione.

    I due punti sopra riportati sono tratti dallo standard C99 che include questa bella nota a piè di pagina molto utile:

    [Il nome ” lvalue ” viene originariamente dall’espressione di assegnazione E1 = E2, in cui l’operando di sinistra E1 deve essere un lvalue (modificabile). Forse è meglio considerare come un object “valore di localizzazione”. Quello che a volte viene chiamato “rvalore” è in questo standard internazionale descritto come il “valore di un’espressione”. ]

    Il valore del localizzatore è chiamato lvalue , mentre il valore risultante dalla valutazione di tale posizione è chiamato valore . Esatto, secondo lo standard C ++ (che parla della conversione da lvalue a rvalue):

    4.1 / 2: il valore contenuto nell’object indicato dal lvalue è il risultato del valore.

    Conclusione

    Usando la semantica sopra, è chiaro ora perché i++ non è un lvalue ma un valore. Poiché l’espressione restituita non si trova più in i (è incrementata!), È solo il valore che può essere di interesse. La modifica di quel valore restituito da i++ avrebbe senso, perché non abbiamo una posizione da cui potremmo leggere di nuovo quel valore. E così lo standard dice che è un valore massimo, e quindi può solo legarsi a un riferimento-a-const.

    Tuttavia, in constrast, l’espressione restituita da ++i è la posizione (lvalue) di i . Provocazione di una conversione da lvalue a rvalue, come in int a = ++i; leggerà il valore fuori di esso. In alternativa, possiamo fare un riferimento ad esso, e leggere il valore più tardi: int &a = ++i; .

    Notare anche le altre occasioni in cui vengono generati rvalue. Ad esempio, tutti i provvisori sono rvalues, il risultato di binario / unario + e meno e tutte le espressioni del valore di ritorno che non sono riferimenti. Tutte quelle espressioni non si trovano in un object con nome, ma portano solo valori piuttosto. Naturalmente questi valori possono essere salvati da oggetti che non sono costanti.

    La prossima versione C ++ includerà i cosiddetti rvalue references che, anche se puntano a non convalidare, possono legarsi a un valore. La logica è quella di essere in grado di “sottrarre” le risorse da quegli oggetti anonimi ed evitare di fare delle copie. Assumendo un tipo di class che ha il prefisso sovraccarico ++ (restituendo Object& ) e postfix ++ (restituendo Object ), il seguente causerebbe prima una copia e, nel secondo caso, ruberà le risorse dal valore rvalue:

     Object o1(++a); // lvalue => can't steal. It will deep copy. Object o2(a++); // rvalue => steal resources (like just swapping pointers) 

    Altre persone hanno affrontato la differenza funzionale tra post e pre-incremento.

    Per quanto riguarda un lvalue , non è ansible assegnare i++ perché non si riferisce a una variabile. Si riferisce a un valore calcolato.

    In termini di assegnazione, entrambi i seguenti non hanno alcun senso nello stesso modo:

     i++ = 5; i + 0 = 5; 

    Poiché il pre-incremento restituisce un riferimento alla variabile incrementata piuttosto che una copia temporanea, ++i è un lvalue.

    Preferire il pre-incremento per motivi di prestazioni diventa particolarmente utile quando si incrementa qualcosa come un object iteratore (ad esempio in STL) che potrebbe essere un po ‘più pesante di un int.

    Sembra che molte persone stiano spiegando come ++i è un lvalue, ma non il perché , come in, perché il comitato per gli standard C ++ ha inserito questa funzione, specialmente alla luce del fatto che C non consente come lvalue. Da questa discussione su comp.std.c ++ , sembra che sia così che puoi prendere il suo indirizzo o assegnarlo a un riferimento. Un esempio di codice estratto dal post di Christian Bau:

        int i;
        extern void f (int * p);
        extern void g (int & p);
    
        f (& ++ i);  / * Sarebbe illegale C, ma i programmatori C
                       non ho perso questa funzione * /
        g (++ i);  / * I programmatori C ++ vorrebbero che questo sia legale * /
        g (i ++);  / * C ++ non legale, e sarebbe difficile da
                       dare questa semantica significativa * /
    
    

    A proposito, se i capita di essere un tipo predefinito, le istruzioni di assegnazione come ++i = 10 invocano un comportamento indefinito , perché i viene modificato due volte tra i punti di sequenza.

    Sto ottenendo l’errore lvalue quando provo a compilare

     i++ = 2; 

    ma non quando lo cambio

     ++i = 2; 

    Questo perché l’operatore prefisso (++ i) modifica il valore in i, quindi restituisce i, quindi può ancora essere assegnato a. L’operatore postfisso (i ++) modifica il valore in i, ma restituisce una copia temporanea del vecchio valore , che non può essere modificata dall’operatore di assegnazione.


    Risposta alla domanda originale :

    Se stai parlando di utilizzare gli operatori di incremento in una dichiarazione da soli, come in un ciclo for, non fa alcuna differenza. Il preincremento sembra essere più efficiente, perché il postincremento deve incrementarsi e restituire un valore temporaneo, ma un compilatore ottimizzerà questa differenza.

     for(int i=0; i 

    equivale a

     for(int i=0; i 

    Le cose diventano un po 'più complicate quando si utilizza il valore restituito dell'operazione come parte di una dichiarazione più ampia.

    Anche le due semplici affermazioni

     int i = 0; int a = i++; 

    e

     int i = 0; int a = ++i; 

    sono diversi. Quale operatore di incremento si sceglie di utilizzare come parte di istruzioni multi-operatore dipende da quale sia il comportamento previsto. In breve, no, non puoi sceglierne solo uno. Devi capire entrambi.

    Incremento pre POD:

    Il pre-incremento dovrebbe agire come se l’object fosse incrementato prima dell’espressione ed essere utilizzabile in questa espressione come se ciò accadesse. Pertanto, il comitato per gli standard C ++ ha deciso che può essere utilizzato anche come valore l.

    Incremento Post POD:

    Il post-incremento dovrebbe incrementare l’object POD e restituire una copia da usare nell’espressione (vedere la sezione 5.2.6 della sezione 5.2.6). Siccome una copia non è in realtà una variabile rendendola un valore-l non ha alcun senso.

    Oggetti:

    L’incremento pre e post sugli oggetti è semplicemente lo zucchero sintattico del linguaggio che fornisce un mezzo per chiamare i metodi sull’object. Quindi tecnicamente gli oggetti non sono limitati dal comportamento standard della lingua, ma solo dalle restrizioni imposte dalle chiamate al metodo.

    È compito dell’implementatore di questi metodi far sì che il comportamento di questi oggetti rispecchi il comportamento degli oggetti POD (non è richiesto ma è previsto).

    Pre-incremento degli oggetti:

    Il requisito (comportamento atteso) qui è che gli oggetti vengono incrementati (ovvero dipendenti dall’object) e il metodo restituisce un valore che è modificabile e assomiglia all’object originale dopo che è avvenuto l’incremento (come se l’incremento fosse avvenuto prima di questa dichiarazione).

    Per fare questo è un problema e richiede solo che il metodo restituisca un riferimento a se stesso. Un riferimento è un valore l e quindi si comporterà come previsto.

    Oggetti Post-incremento:

    Il requisito (comportamento atteso) qui è che l’object viene incrementato (allo stesso modo del pre-incremento) e il valore restituito assomiglia al vecchio valore e non è mutabile (in modo che non si comporti come un valore l) .

    Non-Mutevole:
    Per fare questo devi restituire un object. Se l’object viene utilizzato all’interno di un’espressione, verrà copiato in una variabile temporanea. Le variabili temporanee sono const e quindi non modificabili e si comportano come previsto.

    Sembra il vecchio valore:
    Questo si ottiene semplicemente creando una copia dell’originale (probabilmente usando il costruttore di copie) prima di apportare modifiche. La copia dovrebbe essere una copia profonda altrimenti qualsiasi modifica apportata all’originale influirà sulla copia e quindi lo stato cambierà in relazione all’espressione usando l’object.

    Allo stesso modo del pre-incremento:
    Probabilmente è meglio implementare l’incremento post in termini di pre-incremento in modo da ottenere lo stesso comportamento.

     class Node // Simple Example { /* * Pre-Increment: * To make the result non-mutable return an object */ Node operator++(int) { Node result(*this); // Make a copy operator++(); // Define Post increment in terms of Pre-Increment return result; // return the copy (which looks like the original) } /* * Post-Increment: * To make the result an l-value return a reference to this object */ Node& operator++() { /* * Update the state appropriatetly */ return *this; } }; 

    Per quanto riguarda LValue

    • In C (e Perl per esempio), ++ii++ sono LValues.

    • In C++ , i++ non è e LValue ma ++i è.

      ++i è equivalente a i += 1 , che è equivalente a i = i + 1 .
      Il risultato è che stiamo ancora trattando lo stesso object i .
      Può essere visto come:

       int i = 0; ++i = 3; // is understood as i = i + 1; // i now equals 1 i = 3; 

      i++ d’altra parte possono essere visualizzati come:
      Per prima cosa usiamo il valore di i , quindi incrementiamo l’ object i .

       int i = 0; i++ = 3; // would be understood as 0 = 3 // Wrong! i = i + 1; 

    (modifica: aggiornata dopo un primo tentativo blotched).

    La differenza principale è che i ++ restituisce il valore di pre-incremento mentre ++ i restituisce il valore post-incremento. Di solito uso ++ i a meno che non abbia una ragione molto convincente per usare i ++ – cioè, se davvero ho bisogno del valore di pre-incremento.

    IMHO è buona pratica usare il modulo ‘++ i’. Mentre la differenza tra pre e post-incremento non è realmente misurabile quando si confrontano interi o altri POD, la copia aggiuntiva dell’object che si deve creare e restituire quando si utilizza ‘i ++’ può rappresentare un impatto significativo sulle prestazioni se l’object è piuttosto costoso copiare o incrementare frequentemente.

    A proposito: evitare di utilizzare più operatori di incremento sulla stessa variabile nella stessa istruzione. Si entra in un caos di “dove sono i punti di sequenza” e l’ordine non definito delle operazioni, almeno in C. Penso che un po ‘di quello sia stato ripulito in Java nd C #.

    Forse questo ha qualcosa a che fare con il modo in cui viene implementato il post-incremento. Forse è qualcosa del genere:

    • Crea una copia del valore originale in memoria
    • Incrementa la variabile originale
    • Restituire la copia

    Poiché la copia non è né una variabile né un riferimento alla memoria allocata dynamicmente, non può essere un valore l.

    Come traduce questa espressione il compilatore? a++

    Sappiamo che vogliamo restituire la versione non completata di a , la vecchia versione di a prima dell’incremento. Vogliamo anche incrementare a come effetto collaterale. In altre parole, stiamo restituendo la vecchia versione di a , che non rappresenta più lo stato corrente di a , non è più la variabile stessa.

    Il valore che viene restituito è una copia di a che viene inserita in un registro . Quindi la variabile viene incrementata. Quindi qui non stai restituendo la variabile stessa, ma stai restituendo una copia che è un’ quadro separata ! Questa copia viene temporaneamente archiviata in un registro e quindi viene restituita. Ricorda che un lvalue in C ++ è un object che ha una posizione identificabile in memoria . Ma la copia è memorizzata all’interno di un registro nella CPU, non in memoria. Tutti i rvalue sono oggetti che non hanno una posizione identificabile in memoria . Questo spiega perché la copia della vecchia versione di a è un valore rvalue, perché viene temporaneamente memorizzata in un registro. In generale, qualsiasi copia, valore temporaneo o il risultato di espressioni lunghe come (5 + a) * b sono memorizzate nei registri e quindi vengono assegnate alla variabile, che è un lvalue.

    L’operatore postfisso deve memorizzare il valore originale in un registro in modo che possa restituire come risultato il valore nonincrementato. Considera il seguente codice:

     for (int i = 0; i != 5; i++) {...} 

    Questo ciclo chiuso conta fino a cinque, ma i++ è la parte più interessante. In realtà sono due istruzioni in 1. Per prima cosa dobbiamo spostare il vecchio valore di i nel registro, quindi incrementiamo i . Nel codice pseudo-assembly:

     mov i, eax inc i 

    registro eax ora contiene la vecchia versione di i come copia. Se la variabile risiede nella memoria principale, potrebbe richiedere molto tempo alla CPU per ottenere la copia dalla memoria principale e spostarla nel registro. Di solito è molto veloce per i moderni sistemi di computer, ma se il tuo ciclo continua iterando centomila volte, tutte quelle operazioni extra iniziano a sumrsi! Sarebbe una penalità significativa per le prestazioni.

    I compilatori moderni sono in genere abbastanza intelligenti da ottimizzare questo lavoro extra per i tipi di puntatori e interi. Per tipi di iteratori più complicati o tipi di classi, questo lavoro extra potrebbe potenzialmente essere più costoso.

    Che dire dell’incremento del prefisso ++a ?

    Vogliamo restituire la versione incrementata di a , la nuova versione di a dopo l’incremento. La nuova versione di a rappresenta lo stato corrente di a , perché è la variabile stessa.

    Il primo a viene incrementato. Dal momento che vogliamo ottenere la versione aggiornata di a , perché non restituire semplicemente la variabile a ? Non è necessario creare una copia temporanea nel registro per generare un valore. Ciò richiederebbe un lavoro extra non necessario. Quindi, restituiamo la variabile stessa come un valore.

    Se non abbiamo bisogno del valore nonincrementato, non è necessario il lavoro extra di copiare la vecchia versione di a in un registro, operazione eseguita dall’operatore postfisso. Questo è il motivo per cui dovresti usare solo a++ se davvero hai bisogno di restituire il valore non riconosciuto. Per tutti gli altri scopi, basta usare ++a . Usando abitualmente le versioni di prefisso, non dobbiamo preoccuparci se la differenza di prestazioni è importante.

    Un altro vantaggio dell’utilizzo di ++a è che esprime l’intento del programma più direttamente: voglio solo incrementare a ! Tuttavia, quando vedo a++ nel codice di qualcun altro, mi chiedo perché vogliono restituire il vecchio valore? Cosa serve?

    Un esempio:

     var i = 1; var j = i++; // i = 2, j = 1 

    e

     var i = 1; var j = ++i; // i = 2, j = 2 

    C #:

     public void test(int n) { Console.WriteLine(n++); Console.WriteLine(++n); } /* Output: n n+2 */