¿Por qué es ++i considerado un valor l, pero i++ no lo es?


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

Author: yesraaj, 2008-12-16

12 answers

Así como otro answerer señaló ya la razón por la que ++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 de la segunda regla es permitir inicializar una referencia usando un literal, cuando la referencia es una referencia a const:{[15]]}

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

¿Por qué introducimos un rvalue en todo lo que pueda preguntar? Bueno, estos términos surgen al construir las reglas del lenguaje para estas dos situaciones:

  • Queremos tener un valor localizador. Que representará un 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 bonita nota al pie bastante útil:

[El nombre "lvalue" viene originalmente de la expresión de asignación E1 = E2, en el que el operando izquierdo E1 es debe ser un lvalue (modificable). Es quizás mejor considerado como representación de un objeto " localizador valor’’. Lo a veces se llama "rvalue" está en esta Internacional Estándar descrito como el " valor de expresion’’. ]

El valor del localizador se llama lvalue, mientras que el valor resultante de evaluar esa ubicación se llama rvalue. Eso es correcto también de acuerdo con el estándar C++ (hablando de la conversión lvalue a rvalue):

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

Conclusión

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

Sin embargo, en constrast, 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 ella. Alternativamente, podemos hacer un punto de referencia a él, y leer el valor más tarde: int &a = ++i;.

Tenga en cuenta también las otras ocasiones en las que se generan rvalues. Por ejemplo, todos los temporales son rvalues, el resultado de binario / unario + y menos y todas las expresiones de valor devuelto que no son referencias. Todo esas expresiones no se encuentran en un objeto con nombre, sino que solo llevan valores. Por supuesto, esos valores pueden ser respaldados por objetos que no son constantes.

La próxima versión de C++ incluirá los llamados rvalue references que, a pesar de que apuntan a nonconst, pueden vincularse a un rvalue. La razón es poder "robar" recursos de esos objetos anónimos, y evitar que las copias hagan eso. Asumiendo un tipo de clase que ha sobrecargado prefix ++ (devolviendo Object&) y postfix ++ (devolviendo Object), lo siguiente causaría una copia primero, y para el segundo caso robaría los recursos del rvalue:

Object o1(++a); // lvalue => can't steal. It will deep copy.
Object o2(a++); // rvalue => steal resources (like just swapping pointers)
 23
Author: Johannes Schaub - litb,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-17 10:42:14

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

En lo que respecta a ser un lvalue, i++ no se puede asignar porque no se refiere a una variable. Se refiere a un valor calculado.

En términos de asignación, ambos de los siguientes no tienen sentido de la misma manera:

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

Debido a que el pre-incremento devuelve una referencia a la variable incrementada en lugar de una copia temporal, ++i es un lvalue.

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

 31
Author: mackenir,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-16 15:13:23

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 ninguno de los dos como lvalues. De esta discusión sobre comp.ETS.c++, parece que es para que pueda tomar su dirección o asignar a una referencia. Un ejemplo de código extraído del post de Christian Bau:

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

   f (&++i);   /* Would be illegal C, but C programmers
                  havent missed this feature */
   g (++i);    /* C++ programmers would like this to be legal */
   g (i++);    /* Not legal C++, and it would be difficult to
                  give this meaningful semantics */

Por cierto, si i pasa a ser un tipo incorporado, luego instrucciones de asignación como ++i = 10invocan comportamiento indefinido, porque i se modifica dos veces entre puntos de secuencia.

 10
Author: Nietzche-jou,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-16 15:42:24

Recibo el error lvalue cuando intento compilar

i++ = 2;

, Pero no cuando lo cambio a

++i = 2;

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


Respuesta a la pregunta original :

Si estás hablando de el uso de los operadores de incremento en una declaración por sí mismos, como en un bucle for, realmente no hace ninguna diferencia. Preincremento parece ser más eficiente, ya que de postincremento tiene el incremento de la misma y devolver un valor temporal, sino un compilador puede optimizar esta diferencia de distancia.

for(int i=0; i<limit; i++)
...

Es lo mismo que

for(int i=0; i<limit; ++i)
...

Las cosas se complican un poco más cuando se utiliza el valor devuelto de la operación como parte de una sentencia más grande.

Incluso los dos simples declaraciones

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 las sentencias multi-operador depende de cuál sea el comportamiento previsto. En resumen, no, no puedes elegir uno. Tienes que entender ambas cosas.

 5
Author: Bill the Lizard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-16 15:54:58

POD Pre incremento:

El pre-incremento debe actuar como si el objeto se incrementara antes de la expresión y ser utilizable 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 Post incremento:

El post-incremento debe incrementar el objeto POD y devolver una copia para su uso en la expresión (Ver n2521 Sección 5.2.6). Como una copia no es realmente una variable, por lo que es un valor l no hace ninguna sentido.

Objetos:

Pre y Post incremento en objetos es solo azúcar sintáctica del lenguaje proporciona un medio para llamar a los métodos en el 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 los 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 necesario, pero se espera).

Objetos Pre-incremento:

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

Para hacer esto es siple y solo requiere que el método devuelva una referencia a it-self. Una referencia es un valor l y por lo tanto se comportará como se espera.

Objetos Post-incremento:

El requisito (comportamiento esperado) aquí es que el objeto se incremente (de la misma manera que el pre-incremento) y el valor devuelto se vea como el valor antiguo y no sea mutable (para que no se comporte como un valor l).

No mutable:
Para hacer esto debe devolver un objeto. Si el objeto se está utilizando dentro de una expresión, se copiará en una variable temporal. Las variables temporales son constantes y por lo tanto no mutables y se comportan como previsto.

Se parece al valor antiguo:
Esto se logra simplemente creando una copia del original (probablemente usando el constructor de copia) antes de hacer 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 usa el objeto.

De la misma manera que el pre-incremento:
Probablemente sea mejor implementar el post incremento en términos de pre-incremento para obtener 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;
     }
};
 4
Author: Loki Astari,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-16 19:44:24

Respecto a LValue

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

  • 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.
    Puede ser visto como:

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

    i++ por otro lado podría ser visto como:
    Primero usamos la valor de i, luego incrementa el objeto i.

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

(edit: updated after a blotched first-attempt).

 1
Author: Renaud Bompuis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-17 03:08:50

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

En mi humilde opinión, es una buena práctica utilizar el formulario '++i'. Mientras que la diferencia entre pre - y post-incremento no es realmente medible cuando se comparan enteros u otros PODs, la copia de objetos adicionales que tiene que hacer y devolver cuando se usa 'i++' puede representar un impacto significativo en el rendimiento si el objeto es bastante costoso de copiar o se incrementa con frecuencia.

 0
Author: Timo Geusch,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-16 14:48:40

Por cierto - evite usar múltiples operadores de incremento 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#.

 0
Author: Paul Tomblin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-16 14:50:13

Tal vez esto tenga algo que ver con la forma en que se implementa el post-incremento. Tal vez sea algo como esto:

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

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

 0
Author: pyon,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-16 19:36:19

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

, sabemos que Nos quiere devolver el unincremented versión de a, la versión antigua de un antes de el incremento. También queremos incrementar a como un efecto secundario. En otras palabras, estamos devolviendo la versión antigua de a, que ya no representa el estado actual de a, ya no es la variable en sí.

El valor que se devuelve es una copia de a que se coloca en un regístrese. Entonces la variable se incrementa. Así que aquí no está devolviendo la variable en sí, sino que 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 lvalue en C++ es un objeto que tiene una ubicación identificable en memoria. Pero la copia se almacena dentro de un registro en la CPU, no en la memoria. Todos los rvalues son objetos que no tienen una ubicación identificable en memoria. Eso explica por qué la copia de la versión antigua de a es un rvalue, porque se almacena temporalmente en un registro. En general, las copias, los valores temporales o los resultados de expresiones largas como (5 + a) * b se almacenan en registros, y luego se asignan a la variable, que es un lvalue.

El operador postfix debe almacenar el valor original en un registro para que pueda devolver el valor no aumentado como resultado. Considere lo siguiente código:

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

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

mov i, eax
inc i

eax register ahora contiene la versión antigua de i como una copia. Si la variable i reside en la memoria principal, la CPU podría tardar mucho tiempo en obtener la copia desde la memoria principal y moverla en el registro. Eso suele ser muy rápido para los sistemas informáticos modernos, pero si su for-loop itera cien mil veces, ¡todas esas operaciones adicionales comienzan a sumar! Sería una penalización de rendimiento significativa.

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

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

Queremos devolver el incrementa versión de a, la nueva versión de a después de el incremento. La nueva versión de a representa el estado actual de a, porque es la variable en sí.

Primero a se incrementa. Ya que queremos obtener la versión actualizada de a, ¿por qué no devolver la variable a misma? No necesitamos hacer una copia temporal en el registro para generar un rvalue. Eso requeriría trabajo extra innecesario. Así que sólo devolver la variable en sí como un lvalue.

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

Otra ventaja de usar ++a es que expresa la intención del programa 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 antiguo? ¿Para qué es?

 0
Author: Galaxy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-19 18:20:13

Un ejemplo:

var i = 1;
var j = i++;

// i = 2, j = 1

Y

var i = 1;
var j = ++i;

// i = 2, j = 2
 -2
Author: weiran,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-16 14:48:50

C#:

public void test(int n)
{
  Console.WriteLine(n++);
  Console.WriteLine(++n);
}

/* Output:
n
n+2
*/
 -4
Author: LeppyR64,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-12-16 16:38:50