Lo que hizo i = i ++ + 1; legal en C ++ 17?

186

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_ty 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.

De [N3337 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.

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 ino es "otro efecto secundario" ni "usar el valor del mismo objeto escalar" ya que la expresión id es un valor l.

Transeúnte
fuente
23
Ha identificado la razón por la cual: en C ++ 17, el operando derecho se secuencia antes que el operando izquierdo. En C ++ 11 no hubo tal secuenciación. ¿Cuál es, precisamente, tu pregunta?
Robᵩ
44
@ Robᵩ Ver la última oración.
Pasador antes del
77
¿Alguien tiene un vínculo con la motivación para este cambio? Me gustaría que un analizador estático pueda decir "no quieres hacer eso" cuando te enfrentas a un código como i = i++ + 1;.
77
@NeilButterworth, es del documento p0145r3.pdf : "Refinación del orden de evaluación de expresiones para Idiomatic C ++".
xaizek
9
@NeilButterworth, la sección número 2 dice que esto es contrario a la intuición e incluso los expertos no hacen lo correcto en todos los casos. Eso es casi toda su motivación.
xaizek

Respuestas:

144

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:

El operando derecho se secuencia antes que el operando izquierdo.

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).

Hormiga
fuente
3
¿Es realmente correcto incluir "el acto real de asignación" en los efectos cubiertos por "el operando de la mano izquierda" en ese extracto? El estándar tiene un lenguaje separado sobre la secuencia de la asignación real. Considero que el extracto que ha presentado tiene un alcance limitado a la secuenciación de las subexpresiones de la izquierda y la derecha, que no parece ser suficiente, en combinación con el resto de esa sección, para respaldar bien definición de la declaración del OP.
John Bollinger
11
Corrección: la asignación real todavía se secuencia después del cálculo del valor del operando izquierdo, y la evaluación del operando izquierdo se secuencia después de la evaluación (completa) del operando derecho, entonces sí, ese cambio es suficiente para respaldar la buena definición del OP preguntó sobre. Solo estoy discutiendo los detalles, entonces, pero estos son importantes, ya que pueden tener diferentes implicaciones para diferentes códigos.
John Bollinger
3
@JohnBollinger: Encuentro curioso que los autores de la Norma hagan un cambio que perjudique la eficiencia de la generación de código directo e históricamente no ha sido necesario, y sin embargo se niegan a definir otros comportamientos cuya ausencia es un problema mucho mayor, y que Raramente representaría un impedimento significativo para la eficiencia.
supercat
1
@Kaz: Para asignaciones compuestas, realizar la evaluación del valor del lado izquierdo después del lado derecho permite x -= y;que se procese algo así como en mov eax,[y] / sub [x],eaxlugar de hacerlo mov 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.
supercat
1
@Kaz: Si xy yfuera volatile, eso tendría efectos secundarios. Además, las mismas consideraciones se aplicarían a x += f();, donde se f()modifica x.
supercat
33

Identificaste la nueva oración

El operando derecho se secuencia antes que el operando izquierdo.

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
7

En estándares C ++ anteriores y en C11, la definición del texto del operador de asignación termina con el texto:

Las evaluaciones de los operandos no tienen secuencia.

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:

El operando derecho se secuencia antes que el operando izquierdo.


Como nota al margen, incluso en los estándares más antiguos, todo esto se hizo muy claro, ejemplo de C99:

El orden de evaluación de los operandos no está especificado. Si se intenta modificar el resultado de un operador de asignación o acceder a él después del siguiente punto de secuencia, el comportamiento no está definido.

Básicamente, en C11 / C ++ 11, se equivocaron cuando eliminaron este texto.

Lundin
fuente
1

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):

i = i++;

El + 1es 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 + 1hizo 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 ++).

MM
fuente