loop for based c++ language-lawyer c++17

based - for() c++



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

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 ordenan 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 que se refiere 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 que se refiere 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.


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 secuenciación específicas de la forma del prefijo ++ habrían salvado el día en este ejemplo. Pero postfix ++ es una historia diferente: no ofrece tales garantías. En C ++ 11, los efectos secundarios de = y postfix ++ terminan sin secuencia en relación con 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 por completo 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).


En estándares anteriores de C ++ 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.


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.