Antes de comenzar a gritar comportamiento indefinido, esto se enumera explícitamente en N4659 (C ++ 17)
i = i++ + 1; // the value of i is incremented
Sin embargo, en N3337 (C ++ 11)
i = i++ + 1; // the behavior is undefined
¿Qué cambió?
De lo que puedo reunir, de [N4659 basic.exec]
Excepto donde se indique, las evaluaciones de operandos de operadores individuales y de subexpresiones de expresiones individuales no tienen secuencia. [...] Los cálculos del valor de los operandos de un operador se ordenan antes del cálculo del valor del resultado del operador. Si un efecto secundario en una ubicación de memoria no está secuenciado en relación con otro efecto secundario en la misma ubicación de memoria o un cálculo de valor utilizando el valor de cualquier objeto en la misma ubicación de memoria, y no son potencialmente concurrentes, el comportamiento es indefinido.
Donde el valor se define en [N4659 basic.type]
Para los tipos que se pueden copiar trivialmente, la representación del valor es un conjunto de bits en la representación del objeto que determina un valor , que es un elemento discreto de un conjunto de valores definidos por la implementación
Desde [N3337 basic.exec]
Excepto donde se indique, las evaluaciones de operandos de operadores individuales y de subexpresiones de expresiones individuales no tienen secuencia. [...] Los cálculos del valor de los operandos de un operador se secuencian antes del cálculo del valor del resultado del operador. Si un efecto secundario sobre un objeto escalar no está secuenciado en relación con otro efecto secundario sobre el mismo objeto escalar o un cálculo de valor utilizando el valor del mismo objeto escalar, el comportamiento es indefinido.
Del mismo modo, el valor se define en [N3337 basic.type]
Para los tipos que se pueden copiar trivialmente, la representación del valor es un conjunto de bits en la representación del objeto que determina un valor , que es un elemento discreto de un conjunto de valores definidos por la implementación.
Son idénticos, excepto la mención de concurrencia que no importa, y con el uso de la ubicación de la memoria en lugar del objeto escalar , donde
Los tipos aritméticos, los tipos de enumeración, los tipos de puntero, los tipos de puntero a miembro
std::nullptr_t
y las versiones calificadas por cv de estos tipos se denominan colectivamente tipos escalares.
Lo que no afecta el ejemplo.
Desde [N4659 expr.ass]
El operador de asignación (=) y los operadores de asignación compuesta se agrupan de derecha a izquierda. Todos requieren un valor l modificable como su operando izquierdo y devuelven un valor l referido al operando izquierdo. El resultado en todos los casos es un campo de bits si el operando izquierdo es un campo de bits. En todos los casos, la asignación se secuencia después del cálculo del valor de los operandos derecho e izquierdo, y antes del cálculo del valor de la expresión de asignación. El operando derecho se secuencia antes que el operando izquierdo.
El operador de asignación (=) y los operadores de asignación compuesta se agrupan de derecha a izquierda. Todos requieren un valor l modificable como su operando izquierdo y devuelven un valor l referido al operando izquierdo. El resultado en todos los casos es un campo de bits si el operando izquierdo es un campo de bits. En todos los casos, la asignación se secuencia después del cálculo del valor de los operandos derecho e izquierdo, y antes del cálculo del valor de la expresión de asignación.
La única diferencia es que la última oración está ausente en N3337.
Sin embargo, la última oración no debería tener importancia ya que el operando izquierdo i
no es "otro efecto secundario" ni "usar el valor del mismo objeto escalar" ya que la expresión id es un valor l.
fuente
i = i++ + 1;
.Respuestas:
En C ++ 11, el acto de "asignación", es decir, el efecto secundario de modificar el LHS, se secuencia después del cálculo del valor del operando correcto. Tenga en cuenta que esta es una garantía relativamente "débil": produce secuenciación solo en relación con el cálculo del valor del RHS. No dice nada sobre los efectos secundarios que podrían estar presentes en el RHS, ya que la aparición de efectos secundarios no es parte del cálculo del valor . Los requisitos de C ++ 11 no establecen una secuencia relativa entre el acto de asignación y los efectos secundarios del RHS. Esto es lo que crea el potencial para UB.
La única esperanza en este caso es cualquier garantía adicional hecha por operadores específicos utilizados en RHS. Si el RHS usara un prefijo
++
, las propiedades de secuencia específicas de la forma del prefijo de++
hubieran salvado el día en este ejemplo. Pero postfix++
es una historia diferente: no ofrece tales garantías. En C ++ 11, los efectos secundarios=
y postfix++
terminan sin secuencia entre sí en este ejemplo. Y eso es UB.En C ++ 17 se agrega una oración adicional a la especificación del operador de asignación:
En combinación con lo anterior, ofrece una garantía muy sólida. Secuencia todo lo que sucede en el RHS (incluidos los efectos secundarios) antes de todo lo que sucede en el LHS. Dado que la asignación real se secuencia después de LHS (y RHS), esa secuencia adicional aísla completamente el acto de asignación de cualquier efecto secundario presente en RHS. Esta secuenciación más fuerte es lo que elimina la UB anterior.
(Actualizado para tener en cuenta los comentarios de @John Bollinger).
fuente
x -= y;
que se procese algo así como enmov eax,[y] / sub [x],eax
lugar de hacerlomov eax,[x] / neg eax / add eax,[y] / mov [x],eax
. No veo nada idiódico sobre eso. Si uno tuviera que especificar un orden, el orden más eficiente probablemente sería realizar todos los cálculos necesarios para identificar primero el objeto del lado izquierdo, luego evaluar el operando derecho, luego el valor del objeto izquierdo, pero eso requeriría tener un término por el acto de resolver la identidad del objeto izquierdo.x
yy
fueravolatile
, eso tendría efectos secundarios. Además, las mismas consideraciones se aplicarían ax += f();
, donde sef()
modificax
.Identificaste la nueva oración
e identificó correctamente que la evaluación del operando izquierdo como un valor l es irrelevante. Sin embargo, la secuencia anterior se especifica como una relación transitiva. Por lo tanto, el operando derecho completo (incluido el post-incremento) también se secuencia antes de la asignación. En C ++ 11, solo el cálculo del valor del operando correcto fue secuenciado antes de la asignación.
fuente
En estándares C ++ anteriores y en C11, la definición del texto del operador de asignación termina con el texto:
Lo que significa que los efectos secundarios en los operandos no tienen secuencia y, por lo tanto, un comportamiento definitivamente indefinido si usan la misma variable.
Este texto simplemente se eliminó en C ++ 11, dejándolo algo ambiguo. ¿Es UB o no? Esto se ha aclarado en C ++ 17 donde agregaron:
Como nota al margen, incluso en los estándares más antiguos, todo esto se hizo muy claro, ejemplo de C99:
Básicamente, en C11 / C ++ 11, se equivocaron cuando eliminaron este texto.
fuente
Esta es más información para las otras respuestas y la estoy publicando, ya que a menudo también se pregunta sobre el código a continuación .
La explicación en las otras respuestas es correcta y también se aplica al siguiente código que ahora está bien definido (y no cambia el valor almacenado de
i
):El
+ 1
es un arenque rojo y no está realmente claro por qué el Estándar lo usó en sus ejemplos, aunque sí recuerdo a las personas que discutían en las listas de correo antes de C ++ 11 que tal vez+ 1
hizo una diferencia debido a forzar la conversión temprana del valor a la derecha. lado. Ciertamente, nada de eso se aplica en C ++ 17 (y probablemente nunca se aplicó en ninguna versión de C ++).fuente