¿Por qué ++ se considera un valor l, pero i ++ no?

¿Por qué es ++ i es l-value y i ++ no?

Bueno, ya que otro respondedor señaló que la razón por la cual ++i es un lvalue es pasarlo a una referencia.

 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 

La razón para la segunda regla es permitir inicializar una referencia usando un literal, cuando la referencia es una referencia a const:

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

¿Por qué presentamos un valor en absoluto? Bueno, estos términos aparecen cuando se crean las reglas del lenguaje para estas dos situaciones:

  • Queremos tener un valor de localización. Eso representará una ubicación que contiene un valor que se puede leer.
  • Queremos representar el valor de una expresión.

Los dos puntos anteriores están tomados del estándar C99 que incluye esta agradable nota al pie bastante útil:

[El nombre ” lvalue ” proviene originalmente de la expresión de asignación E1 = E2, en la cual se requiere que el operando izquierdo E1 sea un valor l (modificable). Tal vez sea mejor considerarlo como la representación de un objeto ” valor de localización ”. Lo que a veces se denomina ” valor real ” se encuentra en esta Norma Internacional descrita como el ” valor de una expresión ”. ]

El valor del localizador se llama lvalue , mientras que el valor resultante de la evaluación de esa ubicación se llama valor de r . Eso es correcto de acuerdo con el estándar C ++ (hablando de la conversión lvalue-r-value):

4.1 / 2: el valor contenido en el objeto indicado por lvalue es el resultado rvalue.

Conclusión

Usando la semántica anterior, está claro ahora por qué i++ no es lvalue sino un valor r. Debido a que la expresión devuelta ya no se encuentra en i (¡se incrementa!), Es solo el valor que puede ser de interés. Modificar el valor devuelto por i++ tendría sentido, porque no tenemos una ubicación desde la que podamos leer ese valor nuevamente. Y entonces el Estándar dice que es un valor r, y por lo tanto solo puede vincularse a una referencia a const.

Sin embargo, en contraste, la expresión devuelta por ++i es la ubicación (lvalue) de i . Provocar una conversión lvalue a rvalue, como en int a = ++i; leerá el valor de esto. Alternativamente, podemos hacer un punto de referencia y leer el valor más adelante: int &a = ++i; .

Tenga en cuenta también las otras ocasiones en que se generan valores r. Por ejemplo, todos los temporales son valores en r, resultado de expresiones binarias / unarias + y menos y todas las expresiones de valor de retorno que no son referencias. Todas esas expresiones no están ubicadas en un objeto con nombre, sino que solo llevan valores. Esos valores pueden, por supuesto, ser respaldados por objetos que no son constantes.

La próxima versión de C ++ incluirá las denominadas rvalue references que, a pesar de que apuntan a noconst, pueden vincularse a un valor r. La razón es poder “robar” los recursos de esos objetos anónimos y evitar que las copias lo hagan. Suponiendo un tipo de clase que ha sobrecargado al prefijo ++ (devolver Object& ) y postfix ++ (devolver Object ), lo siguiente provocaría primero una copia y, en el segundo caso, robaría los recursos del valor r:

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

Otras personas han abordado la diferencia funcional entre el post y el pre incremento.

En cuanto a ser un valor i++ , i++ no se puede asignar porque no se refiere a una variable. Se refiere a un valor calculado.

En términos de asignación, ninguno de los siguientes tiene sentido del mismo modo:

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

Como el preincremento devuelve una referencia a la variable incrementada en lugar de una copia temporal, ++i es un valor l.

Preferir el pre-incremento por motivos de rendimiento se convierte en una idea especialmente buena cuando se incrementa algo como un objeto iterador (por ejemplo, en el STL) que bien puede ser un poco más pesado que un int.

Parece que mucha gente está explicando cómo ++i es un lvalue, pero no el por qué , como en, por qué el comité de estándares de C ++ puso esta característica, especialmente a la luz del hecho de que C no permite ni como lvalues. A partir de esta discusión en comp.std.c ++ , parece que es así que puede tomar su dirección o asignarla a una referencia. Un ejemplo de código extraído de la publicación de Christian Bau:

    int i;
    extern void f (int * p);
    extern void g (int y p);

    f (& ++ i);  / * Sería C ilegal, pero los progtwigdores C
                   no se ha perdido esta característica * /
    g (++ i);  / * Los progtwigdores de C ++ quisieran que esto fuera legal * /
    g (i ++);  / * No es legal C ++, y sería difícil
                   dale a esta semántica significativa * /

Por cierto, si resulta que soy un tipo incorporado, las sentencias de asignación como ++i = 10 invocan un comportamiento indefinido , porque i se modifica dos veces entre puntos de secuencia.

Obtengo el error lvalue cuando bash comstackr

 i++ = 2; 

pero no cuando lo cambio a

 ++i = 2; 

Esto se debe a que el operador de prefijo (++ i) cambia el valor en i, luego devuelve i, por lo que todavía puede asignarse. El operador de posfijo (i ++) cambia el valor en i, pero devuelve una copia temporal del valor anterior , que no puede ser modificado por el operador de asignación.


Responder a la pregunta original :

Si está hablando de usar los operadores de incremento en una instrucción por sí mismos, como en un ciclo for, realmente no hace ninguna diferencia. El preincremento parece ser más eficiente, porque el postincremento tiene que incrementarse y devolver un valor temporal, pero un comstackdor optimizará esta diferencia.

 for(int i=0; i 

es lo mismo que

 for(int i=0; i 

Las cosas se vuelven un poco más complicadas cuando utilizas el valor de retorno de la operación como parte de una statement más grande.

Incluso las dos declaraciones simples

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

y

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

son diferentes. El operador de incremento que elija usar como parte de instrucciones de operadores múltiples depende de cuál sea el comportamiento previsto. En resumen, no, no puedes simplemente elegir uno. Tienes que entender ambos.

POD incremento previo:

El preincremento debería actuar como si el objeto se incrementara antes de la expresión y se pudiera usar en esta expresión como si eso sucediera. Por lo tanto, el comité de estándares de C ++ decidió que también se puede usar como un valor l.

POD Incremento de publicación:

El incremento posterior debe incrementar el objeto POD y devolver una copia para usar en la expresión (Consulte n2521 Sección 5.2.6). Como una copia no es en realidad una variable que lo convierte en un valor l, no tiene ningún sentido.

Objetos:

El incremento pre y post en los objetos es solo azúcar sintáctico del lenguaje que proporciona un medio para llamar a los métodos del objeto. Por lo tanto, técnicamente los objetos no están restringidos por el comportamiento estándar del lenguaje, sino solo por las restricciones impuestas por las llamadas a métodos.

Depende del implementador de estos métodos hacer que el comportamiento de estos objetos refleje el comportamiento de los objetos POD (no es obligatorio, sino esperado).

Pre-incremento de objetos:

El requisito (comportamiento esperado) aquí es que los objetos se incrementan (lo que significa que dependen del objeto) y el método devuelve un valor que es modificable y se parece al objeto original después del incremento (como si el incremento hubiera ocurrido antes de esta statement).

Hacer esto es siple y solo requiere que el método devuelva una referencia a sí mismo. Una referencia es un valor l y, por lo tanto, se comportará como se espera.

Objetos post-incremento:

El requerimiento (comportamiento esperado) aquí es que el objeto se incrementa (de la misma manera que el pre-incremento) y el valor devuelto se ve como el valor anterior y no es mutable (para que no se comporte como un valor l) .

No mutable:
Para hacer esto deberías devolver un objeto. Si el objeto se está utilizando dentro de una expresión, se copiará en una variable temporal. Las variables temporales son const y por lo tanto no serán mutables y se comportarán como se espera.

Parece el valor anterior:
Esto se logra simplemente creando una copia del original (probablemente utilizando el constructor de copia) antes de realizar cualquier modificación. La copia debe ser una copia profunda; de lo contrario, cualquier cambio en el original afectará a la copia y, por lo tanto, el estado cambiará en relación con la expresión que utiliza el objeto.

De la misma manera que el pre-incremento:
Probablemente sea mejor implementar el incremento posterior en términos de preincremento para que obtenga el mismo comportamiento.

 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; } }; 

En cuanto a LValue

  • En C (y Perl por ejemplo), ni ++i ni i++ son LValores.

  • En C++ , i++ no es y LValue pero ++i es.

    ++i es equivalente a i += 1 , que es equivalente a i = i + 1 .
    El resultado es que todavía estamos tratando con el mismo objeto i .
    Se puede ver como:

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

    i++ por otro lado podría verse como:
    Primero usamos el valor de i , luego incrementamos el objeto i .

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

(editar: actualizado después de un primer bash borroso).

La principal diferencia es que i ++ devuelve el valor de preincremento, mientras que ++ i devuelve el valor de incremento posterior. Normalmente uso ++ i a menos que tenga una razón muy convincente para usar i ++, es decir, si realmente necesito el valor de incremento previo.

En mi humilde opinión, es una buena práctica usar la forma ‘++ i’. Si bien la diferencia entre el incremento previo y posterior no se puede medir realmente cuando se comparan enteros u otros POD, la copia adicional del objeto que se debe hacer y devolver al usar ‘i ++’ puede representar un impacto significativo en el rendimiento si el objeto es bastante caro para copiar o boost con frecuencia.

Por cierto, evite usar operadores de incrementos múltiples en la misma variable en la misma instrucción. Te metes en un lío de “dónde están los puntos de secuencia” y el orden indefinido de las operaciones, al menos en C. Creo que algo de eso se limpió en Java nd C #.

Quizás esto tiene algo que ver con la forma en que se implementa el incremento posterior. Quizás es algo como esto:

  • Crea una copia del valor original en la memoria
  • Incrementa la variable original
  • Devuelve la copia

Como la copia no es ni una variable ni una referencia a la memoria asignada dinámicamente, no puede ser un valor l.

¿Cómo el traductor traduce esta expresión? a++

Sabemos que queremos devolver la versión no mejorada de a , la versión anterior de a antes del incremento. También queremos incrementar a como un efecto secundario. En otras palabras, estamos devolviendo la versión anterior de a , que ya no representa el estado actual de a , ya no es la variable misma.

El valor que se devuelve es una copia de a que se coloca en un registro . Entonces la variable se incrementa. ¡Aquí no devuelve la variable en sí, pero está devolviendo una copia que es una entidad separada ! Esta copia se almacena temporalmente dentro de un registro y luego se devuelve. Recuerde que un valor l en C ++ es un objeto que tiene una ubicación identificable en la memoria . Pero la copia se almacena dentro de un registro en la CPU, no en la memoria. Todos los valores r son objetos que no tienen una ubicación identificable en la memoria . Eso explica por qué la copia de la versión anterior de a es un valor r, porque se almacena temporalmente en un registro. En general, cualquier copia, valor temporal o resultado de expresiones largas como (5 + a) * b se almacenan en registros, y luego se asignan a la variable, que es un valor l.

El operador de posfijo debe almacenar el valor original en un registro para que pueda devolver el valor no incrementado como resultado. Considera el siguiente código:

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

Este for-loop cuenta hasta cinco, pero i++ es la parte más interesante. En realidad, son dos instrucciones en 1. Primero, tenemos que mover el valor anterior de i al registro, luego incrementamos i . En código de pseudoensamblaje:

 mov i, eax inc i 

eax registro eax ahora contiene la versión anterior de i como copia. Si la variable i reside en la memoria principal, la CPU puede tardar mucho tiempo en obtener la copia desde la memoria principal y moverla al registro. Eso suele ser muy rápido para los sistemas informáticos modernos, pero si su bucle for itera cientos de miles de veces, ¡todas esas operaciones adicionales comienzan a sumrse! Sería una penalización de rendimiento significativa.

Los comstackdores modernos suelen ser lo suficientemente inteligentes como para optimizar este trabajo adicional para tipos enteros y de punteros. Para tipos de iteradores más complicados, o tal vez tipos de clase, este trabajo extra podría ser potencialmente más costoso.

¿Qué pasa con el incremento de prefijo ++a ?

Queremos devolver la versión incrementada de a , la nueva versión de a después del incremento. La nueva versión de a representa el estado actual de a , porque es la variable misma.

Primero a se incrementa. Como queremos obtener la versión actualizada de a , ¿por qué no solo devuelve la variable a sí misma ? No necesitamos hacer una copia temporal en el registro para generar un valor r. Eso requeriría trabajo adicional innecesario. Así que solo devolvemos la variable en sí misma como un valor l.

Si no necesitamos el valor no incrementado, no hay necesidad del trabajo adicional de copiar la versión anterior de a en un registro, que es realizado por el operador de postfix. Es por eso que solo debe usar a++ si realmente necesita devolver el valor no incrementado. Para todos los demás fines, solo use ++a . Al usar habitualmente las versiones de prefijo, no tenemos que preocuparnos sobre si la diferencia de rendimiento importa.

Otra ventaja de usar ++a es que expresa la intención del progtwig más directamente: ¡solo quiero incrementar a ! Sin embargo, cuando veo a++ en el código de otra persona, me pregunto por qué quieren devolver el valor anterior. ¿Para qué sirve?

Un ejemplo:

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

y

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

DO#:

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