En C++ 11, ¿`i+=++ i+1` muestra un comportamiento indefinido?
c++11 language-lawyer (5)
No existe una clara necesidad de un comportamiento no definido aquí
Sin duda, un argumento que lleva a UB se puede dar, como he indicado en la pregunta, y el que se ha repetido en las respuestas dadas hasta ahora. Sin embargo, esto implica una lectura estricta de 5.17: 7, que es a la vez contradictorio en sí mismo y en contradicción con las declaraciones explícitas de 5,17: 1 sobre asignación compuesta. Con una lectura más débil de 5.17: 7 las contradicciones desaparecen, al igual que el argumento a favor de la UB. De ahí mi conclusión es ni que hay UB aquí, ni de que hay un comportamiento claramente definidos, pero el texto de la norma es inconsistente, y debe ser modificado para dejar en claro lo que prevalece la lectura(Y supongo que esto significa un defecto informe debe ser escrito). Por supuesto, uno podría invocar aquí la cláusula de resguardo en la norma (en la nota 1.3.24) que las evaluaciones en la que fallaron el estándar para definir el comportamiento [sin ambigüedad y la auto-consistente] son un comportamiento indefinido, pero que haría cualquier uso de las asignaciones de compuestos (incluidos los operadores de prefijo de aumento / disminución) en UB, algo que podría atraer a algunos ejecutores, pero ciertamente no es para los programadores.
En lugar de discutir el problema dado, permítanme presentar un ejemplo ligeramente modificada que pone de manifiesto la inconsistencia con mayor claridad. Suponga que uno ha definido
int& f (int& a) { return a; }
una función que no hace nada y devuelve su argumento (valor-I). Ahora modifique el ejemplo de
n += f(++n) + 1;
Tenga en cuenta que, si bien algunas condiciones adicionales sobre la secuencia de llamadas a funciones se dan en la norma, esto no parece a primera vista para efectuar el ejemplo, ya que no hay efectos secundarios en absoluto de la llamada de función (ni siquiera a nivel local dentro de la función), como la incrementación sucede en la expresión argumento para f
cuya evaluación no está sujeto a las condiciones adicionales. De hecho, apliquemos el argumento crucial para un comportamiento indefinido (CAUB), es decir, 5.17: 7, que dice que el comportamiento de una asignación de estos compuestos es el equivalente a la de (en este caso)
n = n + f(++n) + 1;
excepto que n
se evalúa sólo una vez (una excepción que no hace ninguna diferencia en este caso). La evaluación de la declaración que acabo de escribir claramente tiene UB (el valor de cálculo de la primera (prvalue) n
en el lado derecho es WRT unsequenced el efecto secundario de la ++
operación, lo que implica el mismo objeto escalar (1,9: 15) y estás muerto ).
Por lo que la evaluación de n += f(++n) + 1
ha indefinido comportamiento, ¿verdad? ¡Incorrecto! Leer en 5.17: 1 que
Con respecto a una llamada de función secuenciado indeterminadamente-, la operación de una asignación de compuesto es una única evaluación. [ Nota : Por lo tanto, una llamada de función no intervendrá entre la conversión-lvalue-a rvalue y el efecto secundario asociado con cualquier operador de asignación solo compuesto. - nota final ]
Este lenguaje está lejos de ser tan preciso como me gustaría que fuera, pero yo no creo que es un tramo de asumir que "indeterminadamente-secuenciado" debe significar "con respecto a la operación de una asignación compuesta". La nota (, sé no normativo) deja claro que la conversión-lvalue-a rvalue es parte del funcionamiento de la asignación compuesto. Ahora es el llamado de f
indeterminadamente-secuenció con respecto al funcionamiento de la asignación compuesto de +=
? No estoy seguro, debido a que la relación ''secuenciado'' se define para los cálculos individuales de valor y los efectos secundarios, no evaluaciones completas de los operadores, que pueden implicar tanto. De hecho, la evaluación de un operador de asignación compuesto implica tresartículos: la conversión-lvalue-a rvalue de su operando de la izquierda, el efecto secundario (la asignación adecuada), y el valor de cálculo de la asignación de compuesto (que se secuenció después de que el efecto secundario, y devuelve el operando izquierdo original como lvalue). Tenga en cuenta que la existencia de la conversión-lvalue-a rvalue nunca se menciona explícitamente en la norma excepto en la nota antes citada ; en particular, la norma no hace ninguna (otra) una declaración en absoluto en cuanto a su secuenciación en relación con otras evaluaciones. Es bastante claro que en el ejemplo de la llamada f
es secuenciado antes de los efectos secundarios y el valor de cómputo +=
(ya que la llamada se produce en el cálculo del valor del operando derecho de+=
), Pero podría ser indeterminadamente-secuenció con respecto a la parte de conversión-lvalue-a rvalue. Recuerdo de mi pregunta que desde el operando izquierdo +=
es un valor-i (y necesariamente), no se puede interpretar la conversión de valor-I-a rvalue que se ha producido como parte del cálculo del valor del operando de la izquierda.
Sin embargo, por el principio del medio excluido, la llamada a f
o bien debe ser indeterminadamente-secuenció con respecto al funcionamiento de la asignación compuesto de +=
, o no indeterminadamente-secuenciado con respecto a ella; en el último caso debe ser secuenciado antes porque no puede ser secuenciado después de que (la llamada de f
ser secuenciado antes de que el efecto secundario de +=
, y siendo la relación anti-simétrica). Así que primero asuma que se indeterminadamente-secuenciado con respecto a la operación. A continuación, la cláusula citada dice que WRT la llamada de f
la evaluación de+=
es una sola operación, y la nota explica que significa que la llamada no debe intervenir entre la conversión-lvalue-a rvalue y el efecto secundario asociado con +=
; tampoco debe ser secuenciado antes de que ambos, o después de ambos. Pero ser secuenciado después de que el efecto secundario no es posible, por lo que debe ser antes de los dos. Esto hace (por transitividad) el efecto secundario de ++
secuenciado antes de la conversión-lvalue-a rvalue, salida UB. Siguiente asumir el llamado de f
se secuencia antes de la operación de +=
. Entonces es, en particular, se secuenció antes de la conversión-lvalue-a rvalue, y de nuevo por transitividad también lo es el efecto secundario de ++
; sin UB en esta rama tampoco.
Conclusión: 5,17: 1 contradice 5.17: 7 si este último es tomada (CAUB) para ser normativa para preguntas de UB resultantes de evaluaciones unsequenced por 1,9: 15. Como ya he dicho CAUB es contradictorio en sí mismo también (por los argumentos indicados en la pregunta), pero esta respuesta es llegar a tiempo, así que voy a dejarlo en esto por ahora.
Tres problemas, y dos propuestas para resolverlos
Tratar de entender lo que el estándar escribe sobre estos asuntos, distingo tres aspectos en los que el texto es difícil de interpretar; todos ellos son de tal naturaleza que el texto es suficientemente claro sobre cuál es el modelo de sus declaraciones se refieren. (Cito los textos al final de los elementos numerados, ya que no sé la marca para reanudar un elemento numerado después de una cita)
El texto de 5.17: 7 es de una simplicidad evidente que, aunque la intención es fácil de entender, nos da poca retención cuando se aplica a situaciones difíciles. Se hace una demanda de barrido (comportamiento equivalente, al parecer en todos los aspectos), pero cuya aplicación está frustrado por la cláusula de excepción. ¿Qué pasa si el comportamiento de
E1 = E1
opE2
es indefinido? Pues bien el deE1
OP= E2
debería ser así. Pero lo que si la UB fue debido aE1
que está siendo evaluado dos veces enE1 = E1
opE2
? Entonces la evaluaciónE1
op= E2
debe suponer que no sea UB, pero si es así, entonces se define como lo que? Esto es como decir que "los jóvenes del segundo gemelo era exactamente igual que la de la primera, salvo que él no murió en el parto." Francamente, creo que este texto, que ha evolucionado poco desde la versión C "Unasignación compuesta de la formaE1 op = E2
se diferencia de la expresión de asignación sencillaE1 = E1 op E2
solo en que el lvalueE1
se evalúa sólo una vez." podría ser adaptado para que coincida con los cambios en el estándar.(5.17) 7 El comportamiento de una expresión de la forma
E1
op= E2
es equivalente aE1 = E1
opE2
excepto queE1
se evalúa sólo una vez. [...]No es tan claro lo que precisamente las acciones (evaluaciones) están entre los cuales se define la relación ''secuenciado''. Se dice (1,9: 12) que la evaluación de una expresión incluye cálculos de valor y la iniciación de efectos secundarios. Aunque esto parece decir que una evaluación puede tener múltiples componentes (atómicos), la relación secuenciada es en realidad la mayoría define (por ejemplo, en 1,9: 14,15) para los componentes individuales, por lo que podría ser mejor leer esto como que la nociónde "evaluación" abarca tanto los cálculos de valor y (iniciación de) los efectos secundarios. Sin embargo, en algunos casos la relación ''secuenciado'' se define para (toda) la ejecución de una expresión de declaración (1,9: 15) o para una llamada de función (5,17: 1), a pesar de que un pasaje en 1,9: 15 evita la última por referirse directamente a las ejecuciones en el cuerpo de una función llamada.
(1,9) 12 Evaluación de una expresión (o una sub-expresión) en general incluye tanto los cálculos de valor (...) y la iniciación de efectos secundarios. [...] 13 Secuenciado anteses una transitivo, relación asimétrica, por pares entre las evaluaciones ejecutadas por un solo hilo [...] 14 Todo efecto valor de cálculo y secundarios asociados con una expresión completa se secuenció antes de cada efecto del valor de cálculo y secundarios asociados con la próxima completo expresión para ser evaluada. [...] 15 Cuando se llama a una función (si la función está en línea), todos los efectos de valor computación y secundarios asociados con cualquier expresión discusión, o con la expresión de sufijo que designa la función llamada, se secuencia antes de la ejecución de cada expresión o declaración en el cuerpo de la función llamada. [...] Cada evaluación de la función de llamada (incluyendo otras llamadas a funciones) ... se secuenció indeterminada con respecto a la ejecución de la llamada de función [...] (5.2.6, 5.17) 1 ...Con respecto a una llamada a la función secuenciado indeterminadamente-, ...
El texto debe reconocer más claramente que una asignación compuesta implica, en contraste con una simple asignación, la acción de ir a buscar el valor asignado previamente a su operando izquierdo; esta acción es como lvalue-a-rvalue conversión, pero no sucede como parte del cálculo del valor de ese operando de la izquierda, ya que no es un prvalue; de hecho, es un problema que 1,9: 12 sólo reconoce dicha acción para la evaluación prvalue . En particular, el texto debe ser más claro sobre el que las relaciones '''' secuenciados se dan para que la acción, en su caso.
(1,9) 12 Evaluación de una expresión ... incluye ... cálculos de valor (incluyendo la determinación de la identidad de un objeto para la evaluación glvalue y ir a buscar un valor previamente asignado a un objeto para la evaluación prvalue)
El segundo punto es el menos directamente relacionados con nuestra pregunta concreta, y creo que se puede resolver simplemente eligiendo un punto de vista claro y reformular pasages que parecen indicar un punto de vista diferente. Teniendo en cuenta que uno de los propósitos principales de los antiguos puntos de secuencia, y ahora la relación ''secuenciado'', era dejar claro que el efecto secundario de los operadores de sufijo de incremento es WRT unsequenced a las acciones secuenciadas después de que el valor de cómputo de ese operador (por lo tanto dando por ejemplo, i = i++
UB), el punto de vista debe ser que individuocálculos de valor y (iniciación de efectos secundarios) individuales son "evaluaciones" para los que "secuenciado antes de que" se puede definir. Por razones prácticas También incluiría dos más tipos de (triviales) "evaluaciones": introducción de la función (por lo que el lenguaje de 1.9: 15 se puede simplificar a: "Cuando se llama a una función ..., todos los efectos de valor computación y secundarios asociados con cualquiera de sus expresiones de argumento, o con la expresión de sufijo que designa la función llamada, se secuencia antes de la entrada de esa función ") y la salida de función (por lo que cualquier acción en el cuerpo de la función se pone por transitividad secuenciado antes de cualquier cosa que requiere el valor de la función; esto solía ser garantizada por un punto de la secuencia, pero el estándar C ++ 11 parece haber perdido dicha garantía, lo que podría hacer que llamar a una función que termina conreturn i++;
potencialmente UB donde esta no es la intención, y se utiliza para estar seguro). Entonces uno puede también ser claro acerca de la relación "indeterminadamente secuenciado" de las funciones de llamadas: para cada llamada a la función, y cada evaluación que no es (directa o indirectamente) parte de la evaluación de esa llamada, se secuenció que la evaluación (ya sea antes o después) wrt tanto a la entrada y salida de dicha llamada de función, y que deberán tener la misma relación en ambos casos (de modo que, en particular, tales acciones externas no se pueden secuenciar después de la entrada función pero antes de la salida de función, como es claramente deseable dentro de un solo hilo).
Ahora para resolver los puntos 1 y 3, que se pueden ver dos caminos (cada uno que afecta a ambos puntos), que tienen diferentes consecuencias para el comportamiento definido o no de nuestro ejemplo:
asignaciones de compuestos con dos operandos, y tres evaluaciones
operaciones compuestas tienen arreglan sus dos operandos habituales, un operador de la izquierda lvalue y un operando de la derecha prvalue. Para resolver la falta de claridad de 3., que está incluido en 1,9: 12 que ir a buscar el valor asignado previamente a un objeto también puede ocurrir en las asignaciones de compuestos (en lugar de solamente para la evaluación prvalue). La semántica de las tareas compount se definen cambiando 5.17: 7 a
En una asignación de compuesto OP
=
, el valor asignado previamente en el objeto a que se refiere por el operando de la izquierda es inverosímil, el operador OP se aplica con este valor como operador de la izquierda y el operando de la derecha de op=
como operando de la derecha, y el valor resultante reemplaza la de el objeto referido por el operando de la izquierda.
(Que da dos evaluaciones, la FETCH y el efecto secundario;. Tercera evaluación es el valor de cálculo trivial del operador compuesto, secuenciado después de que ambos otras evaluaciones)
Para mayor claridad, estado claramente en 1,9: 15 que valore cálculos en operandos se secuenció antes de todos los cálculos de valor asociadas con el operador (en lugar de sólo aquellos para el resultado del operador ), que asegura que la evaluación de la operador de la izquierda lvalue se secuencia antes de ir a buscar su valor (uno apenas puede imaginar lo contrario), y también secuencias de cálculo del valor de la derecha operando antes que buscar, excluyendo así UB en nuestro ejemplo. Mientras que en él, no veo ninguna razón para no hacerlo también secuenciar el cálculo del valor de operandos antes de que los efectos secundariosasociado con el operador (ya que claramente debe); Esto haría que mencionar de forma explícita para las asignaciones (compuestos) en 5.17: 1 superfluo. Por otro lado no hablar de que hay que ir a buscar el valor de una asignación compuesta se secuencia antes de su efecto secundario.
asignaciones de compuestos con tres operandos, y dos evaluaciones
A fin de obtener que la pelota en una asignación compount será unsequenced con respecto al valor de cómputo del operando de la derecha, haciendo que nuestro ejemplo UB, la manera más clara parece ser la de dar a los operadores compuestos una tercera implícita (medio) operando , un prvalue , no representado por una expresión separada, pero obtenida por conversión de lvalue-a rvalue desde el operando de la izquierda (esta naturaleza de tres operando corresponde a la forma desarrollada de las asignaciones de compuestos, pero obteniendo el operando centro desde el operando de la izquierda, se asegura que el valor se obtiene de la misma objeto para el que se almacenará el resultado, una garantía fundamental que es sólo vagamente e implícitamente da en la formulación actual a través de la "excepto queE1
se evalúa sólo una vez" cláusula). La diferencia con la solución anterior es que el fetch es ahora una conversión genuina-lvalue-a rvalue (ya que el operando del medio es una prvalue) y se lleva a cabo como parte del valor de cálculo de los operandos a la asignación compuesto , lo que hace naturalmente unsequenced con el valor de cómputo del operando de la derecha. Cabe señalar alguna parte (en una nueva cláusula que describe este operando implícito) que el valor de cómputo del operando de la izquierda se secuencia antes de este lvalue-a- conversión rvalue (claramente debe) Ahora 1,9:. 12 se puede dejar como es, y en lugar de 5,17: 7 propongo
En una asignación de compuesto op
=
con operando de la izquierdaa
(una lvalue), y midlle y operandos derechab
respectivamentec
(ambos prvalues), el operador OP se aplica conb
operando como izquierda yc
operando como derecho, y el valor resultante sustituye la del objeto referido pora
.
(Que da una evaluación, el efecto secundario, con como segunda evaluación el valor de cálculo trivial del operador compuesto, secuenciado después de ella.)
Los cambios todavía aplicables a 1,9: 15 y 5.17: 1 sugeridos en la solución anterior aún podría aplicar, pero no darían nuestro ejemplo original de comportamiento definido. Sin embargo, el ejemplo modificado en la parte superior de esta respuesta todavía habría definido comportamiento, a menos que la parte 5,17: 1 "asignación de compuesto es una sola operación" se desecha o modificado (hay un pasaje similar en 5.2.6 para postfix incremento / decremento) . La existencia de esos pasajes sugiere que separar las operaciones fecth y almacenar dentro de una sola asignacion compuesto o de sufijo incremento / decremento fue no la intención de los que escribió el estándar actual (y, por extensión, haciendo nuestro ejemplo UB), pero esto, por supuesto, es mera conjetura.
Esta pregunta surgió mientras estaba leyendo (las respuestas a) Entonces, ¿por qué i = ++ i + 1 está bien definido en C ++ 11?
Deduzco que la explicación sutil es que (1) la expresión ++i
devuelve un valor l pero +
toma prvalues como operandos, por lo que debe realizarse una conversión de lvalue a prvalue; esto implica obtener el valor actual de ese lvalue (en lugar de uno más que el valor anterior de i
) y, por lo tanto, debe secuenciarse después del efecto secundario del incremento (es decir, actualizar i
) (2) el LHS de la asignación también es un lvalue, por lo que su evaluación de valor no implica obtener el valor actual de i
; mientras que este cálculo del valor no se realiza con el cálculo del valor del RHS, esto no plantea ningún problema (3) el cálculo del valor de la asignación implica la actualización i
(nuevamente), pero se secuencia después del cálculo del valor de su RHS, y por lo tanto después del actualización anterior a i
; No hay problema.
Bien, entonces no hay UB allí. Ahora mi pregunta es qué pasa si uno cambia el operador de asignación de =
a +=
(o un operador similar).
¿La evaluación de la expresión
i += ++i + 1
conduce a un comportamiento indefinido?
Como yo lo veo, el estándar parece contradecirse aquí. Dado que el LHS de +=
sigue siendo un valor l (y su RHS sigue siendo un valor prve), se aplica el mismo razonamiento anterior en cuanto a (1) y (2); no hay un comportamiento indefinido en la evaluación de los operandos en +=
. En cuanto a (3), el funcionamiento de la asignación compuesta +=
(más precisamente el efecto secundario de esa operación, su cálculo del valor, si es necesario, está en cualquier caso secuenciado después de su efecto secundario) ahora ambos deben recuperar el valor actual de i
, y luego (obviamente secuenciado después, incluso si el estándar no lo dice explícitamente, o de lo contrario la evaluación de dichos operadores siempre invocaría un comportamiento indefinido) agregue el RHS y almacene el resultado nuevamente en i
. Ambas operaciones habrían dado un comportamiento indefinido si no se hubieran secuenciado con el efecto secundario del ++
, pero como se argumentó anteriormente (el efecto secundario del ++
se secuencia antes del cálculo del valor de +
dando el RHS del operador +=
cuyo cálculo del valor se secuencia antes de la operación de esa asignación compuesta), ese no es el caso.
Pero, por otro lado, el estándar también dice que E += F
es equivalente a E = E + F
, excepto que (el valor l) E se evalúa solo una vez. Ahora, en nuestro ejemplo, el cálculo del valor de i
(que es lo que E
está aquí) como lvalue no implica nada que deba secuenciarse con otras acciones, por lo que hacerlo una o dos veces no hace ninguna diferencia; nuestra expresión debe ser estrictamente equivalente a E = E + F
Pero aquí está el problema; ¡es bastante obvio que evaluar i = i + (++i + 1)
daría un comportamiento indefinido! ¿Lo que da? ¿O es esto un defecto del estándar?
Adicional. He modificado ligeramente mi discusión anterior, para hacer más justicia a la distinción adecuada entre los efectos colaterales y los cálculos de valores, y el uso de "evaluación" (como lo hace el estándar) de una expresión para abarcar ambos. Creo que mi principal interrogación no es solo si el comportamiento está definido o no en este ejemplo, sino cómo uno debe leer el estándar para poder decidir esto. Notablemente, si uno toma la equivalencia de E op= F
a E = E op F
como la autoridad final para la semántica de la operación de asignación compuesta (en cuyo caso el ejemplo tiene claramente UB), o simplemente como una indicación de qué operación matemática interviene en la determinación del valor que se asignará (es decir, el identificado por op
, con el LV convertido de valor a valor l del operador de asignación compuesto como operando izquierdo y su RHS como operando derecho). La última opción hace que sea mucho más difícil argumentar a favor de UB en este ejemplo, como he tratado de explicar. Admito que es tentador hacer que la equivalencia sea autoritaria (de modo que las asignaciones compuestas se convierten en una especie de primitivas de segunda clase, cuyo significado viene dado por la reescritura en términos de primitivas de primera clase, por lo que la definición del lenguaje sería simplificada), pero son argumentos bastante fuertes contra esto:
La equivalencia no es absoluta, debido a la excepción "
E
se evalúa solo una vez". Tenga en cuenta que esta excepción es esencial para evitar el uso cuando la evaluación deE
implica un comportamiento indefinido de efectos secundarios, por ejemplo, en el a bastante comúna[i++] += b;
uso. De hecho, creo que no es posible una reescritura absolutamente equivalente para eliminar las asignaciones compuestas; usando un fictivo|||
operador para designar evaluaciones no secuenciadas, uno podría tratar de definirE op= F;
(con operandosint
por simplicidad) como equivalente a{ int& L=E ||| int R=F; L = L + R; }
{ int& L=E ||| int R=F; L = L + R; }
{ int& L=E ||| int R=F; L = L + R; }
, pero luego el ejemplo ya no tiene UB. En cualquier caso, el estándar no nos da ninguna receta renovada.El estándar no trata las asignaciones de compuestos como primitivas de segunda clase para las cuales no es necesaria una definición de semántica por separado. Por ejemplo en 5.17 (énfasis mío)
El operador de asignación (=) y los operadores de asignación compuesta todos agrupan de derecha a izquierda. [...] 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. Con respecto a una llamada de función con secuencia indeterminada, la operación de una asignación compuesta es una única evaluación .
- Si la intención fuera dejar las asignaciones compuestas como meros shorthands para asignaciones simples, no habría ninguna razón para incluirlas explícitamente en esta descripción. La frase final incluso contradice directamente lo que sería el caso si se tomara la equivalencia como autoritativa.
Si uno admite que las asignaciones compuestas tienen una semántica propia, surge el punto de que su evaluación implica (aparte de la operación matemática) más que solo un efecto secundario (la asignación) y una evaluación de valor (secuenciada después de la asignación), pero también una operación sin nombre de recuperar el valor (anterior) del LHS. Esto normalmente se trataría bajo el título de "conversión de valor a valor rvalua", pero hacerlo aquí es difícil de justificar, ya que no hay un operador presente que tome el LHS como un operando rvalue (aunque hay uno en el forma "equivalente"). Es precisamente esta operación sin nombre cuya potencial relación no secuenciada con el efecto secundario de ++
causaría UB, pero esta relación no secuenciada no está explícita en ninguna parte en el estándar, porque la operación sin nombre no lo es. Es difícil justificar UB usando una operación cuya existencia misma está implícita en el estándar.
Sí, es UB!
La evaluación de tu expresión
i += ++i + 1
procede en los siguientes pasos:
5.17p1 (C ++ 11) estados (énfasis mío):
El operador de asignación (=) y los operadores de asignación compuesta todos agrupan de derecha a izquierda. Todos requieren un valor l modificable como su operando izquierdo y devuelven un valor l referente al operando de la izquierda. 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.
¿Qué significa "cálculo de valor"?
1.9p12 da la respuesta:
Acceder a un objeto designado por un glvalue volátil (3.10), modificar un objeto, llamar a una función de E / S de biblioteca o llamar a una función que realiza cualquiera de esas operaciones son todos efectos colaterales, que son cambios en el estado del entorno de ejecución. La evaluación de una expresión (o una subexpresión) en general incluye tanto cálculos de valores (incluida la determinación de la identidad de un objeto para la evaluación de glvalue y obtención de un valor previamente asignado a un objeto para la evaluación prvalue) como la iniciación de efectos secundarios.
Como su código usa un operador de asignación compuesta , 5.17p7 nos dice cómo se comporta este operador:
El comportamiento de una expresión de la forma
E1 op= E2
es equivalente aE1 = E1 op E2 except that
E1 se evalúa solo una vez.
Por lo tanto, la evaluación de la expresión E1 ( == i)
implica tanto determinar la identidad del objeto designado por i
como una conversión de valor l a valor r para obtener el valor almacenado en ese objeto. Pero la evaluación de los dos operandos E1
y E2
no se secuencian entre sí. Por lo tanto, obtenemos un comportamiento indefinido ya que la evaluación de E2 ( == ++i + 1)
inicia un efecto secundario (actualizando i
).
1.9p15:
... Si un efecto secundario en un objeto escalar no se ha secuenciado en relación con otro efecto secundario en el mismo objeto escalar o con un cálculo de valor que utiliza el valor del mismo objeto escalar, el comportamiento no está definido.
Las siguientes declaraciones en sus preguntas / comentarios parecen ser la raíz de su malentendido:
(2) el LHS de la asignación también es un valor l, por lo que su evaluación de valor no implica obtener el valor actual de
i
ir a buscar un valor puede ser parte de una evaluación prvalue. Pero en E + = F, el único prvalue es F, por lo tanto, obtener el valor de E no es parte de la evaluación de la subexpresión E (lvalue)
Si una expresión es un lvalue o rvalue, no dice nada acerca de cómo debe evaluarse esta expresión. Algunos operadores requieren lvalues como operandos y otros requieren valores.
Cláusula 5p8:
Cuando una expresión glvalue aparece como un operando de un operador que espera un valor pr para ese operando, las conversiones estándar lvalue-to-rvalue (4.1), array-to-puntero (4.2) o función-puntero (4.3) son aplicado para convertir la expresión en un valor pr.
En una asignación simple, la evaluación de LHS solo requiere determinar la identidad del objeto. Pero en una asignación compuesta como +=
el LHS debe ser un valor l modificable, pero la evaluación del LHS en este caso consiste en determinar la identidad del objeto y una conversión de valor l a valor r. Es el resultado de esta conversión (que es un valor prve) que se agrega al resultado (también un valor prve) de la evaluación del RHS.
"Pero en E + = F, el único valor prve es F, por lo que el valor de E no es parte de la evaluación de la subexpresión E (lvalue)"
Eso no es verdad, como expliqué anteriormente. En su ejemplo, F
es una expresión prvalue, pero F
puede ser una expresión lvalue. En ese caso, la conversión lvalue-to-rvalue también se aplica a F
5.17p7, como se citó anteriormente, nos dice cuál es la semántica de los operadores de asignación compuesta. La norma establece que el comportamiento de E += F
es el mismo que el de E = E + F
pero E
solo se evalúa una vez. Aquí, la evaluación de E
incluye la conversión lvalue-a-rvalue, porque el operador binario +
requiere que los operandos sean valores r.
La expresion:
i += ++i + 1
no invoca comportamiento indefinido. El método del abogado del idioma requiere que regresemos al informe de defectos que resulta en:
i = ++i + 1 ;
estar bien definido en C ++ 11, que es el informe de defectos 637. Las reglas de secuenciación y el ejemplo no concuerdan , comienza diciendo:
En 1.9 [intro.execution] párrafo 16, la siguiente expresión todavía se muestra como un ejemplo de comportamiento indefinido:
i = ++i + 1;
Sin embargo, parece que las nuevas reglas de secuenciación hacen que esta expresión esté bien definida
La lógica utilizada en el informe es la siguiente:
Se requiere secuenciar el efecto secundario de la asignación después de los cálculos de valor de su LHS y RHS (párrafo 1.17 [expr.ass] 1).
El LHS (i) es un valor l, por lo que su cálculo de valor implica calcular la dirección de i.
Para calcular el valor de RHS (++ i + 1), primero es necesario calcular el valor de la expresión lvalue ++ i y luego hacer una conversión lvalue-a-rvalue en el resultado. Esto garantiza que el efecto secundario de incremento se secuencia antes del cálculo de la operación de suma, que a su vez se secuencia antes del efecto secundario de asignación. En otras palabras, produce un orden bien definido y un valor final para esta expresión.
Entonces en esta pregunta nuestro problema cambia el RHS
que va desde:
++i + 1
a:
i + ++i + 1
debido al borrador de C ++ 11 sección estándar 5.17
Asignación y operadores de asignación compuesta que dice:
El comportamiento de una expresión de la forma E1 op = E2 es equivalente a E1 = E1 op E2, excepto que E1 se evalúa solo una vez. [...]
Entonces ahora tenemos una situación en la que el cálculo de i
en el RHS
no está secuenciado en relación a ++i
y entonces tenemos un comportamiento indefinido. Esto se desprende del párrafo 15 de la sección 1.9
que dice:
Excepto donde se indique, las evaluaciones de operandos de operadores individuales y de subexpresiones de expresiones individuales no son secuenciadas. [Nota: en una expresión que se evalúa más de una vez durante la ejecución de un programa, las evaluaciones no secuenciadas e indeterminadas de sus subexpresiones no necesitan realizarse de manera consistente en diferentes evaluaciones. -finalización] Los cómputos de valores de los operandos de un operador se ordenan antes del cálculo del valor del resultado del operador. Si un efecto secundario en un objeto escalar no se está secuenciando en relación con otro efecto secundario en el mismo objeto escalar o con un cálculo de valor que utiliza el valor del mismo objeto escalar, el comportamiento no está definido.
La forma pragmática de mostrar esto sería usar clang
para probar el código, que genera la siguiente advertencia ( ver en vivo ):
warning: unsequenced modification and access to ''i'' [-Wunsequenced]
i += ++i + 1 ;
~~ ^
para este código:
int main()
{
int i = 0 ;
i += ++i + 1 ;
}
Esto se ve reforzado por este ejemplo de prueba explícito en clang''s
conjunto de pruebas clang''s
para -Wunsequenced :
a += ++a;
Acerca de la descripción de i = ++i + 1
Deduzco que la explicación sutil es que
(1) la expresión
++i
devuelve un valor l pero+
toma prvalues como operandos, por lo que debe realizarse una conversión de lvalue a prvalue;
Probablemente, vea CWG active issue 1642 .
esto implica obtener el valor actual de ese valor l (en lugar de uno más que el valor anterior de
i
) y, por lo tanto, debe secuenciarse después del efecto secundario del incremento (es decir, actualizari
)
La secuencia aquí se define para el incremento (indirectamente, a través de +=
, ver (a) ): el efecto secundario de ++
(la modificación de i
) se secuencia antes del cálculo del valor de la expresión completa ++i
. El último se refiere a calcular el resultado de ++i
, no a cargar el valor de i
.
(2) el LHS de la asignación también es un valor l, por lo que su evaluación de valor no implica obtener el valor actual de
i
; mientras que este cálculo de valor no se realiza con el cálculo del valor del RHS, esto no plantea ningún problema
No creo que esté bien definido en el Estándar, pero estaría de acuerdo.
(3) el cálculo del valor de la tarea en sí implica actualizar
i
(nuevamente),
El cálculo del valor de i = expr
solo es necesario cuando utiliza el resultado, por ej. int x = (i = expr);
o (i = expr) = 42;
. El cálculo del valor en sí mismo no modifica i
.
La modificación de i
en la expresión i = expr
que ocurre debido a =
se llama el efecto secundario de =
. Este efecto secundario se secuencia antes del cálculo de valor de i = expr
- o más bien el cálculo del valor de i = expr
se secuencia después del efecto secundario de la asignación en i = expr
.
En general, el cálculo del valor de los operandos de una expresión se secuencia antes del efecto secundario de esa expresión, por supuesto.
pero se secuencia después del cálculo del valor de su RHS, y por lo tanto después de la actualización anterior a
i
; No hay problema.
El efecto secundario de la asignación i = expr
se secuencia después del cálculo del valor de los operandos i
(A) y expr
de la asignación.
El expr
en este caso es una expresión +
: expr1 + 1
. El cálculo del valor de esta expresión se secuencia después de los cálculos de valor de sus operandos expr1
y 1
.
El expr1
aquí es ++i
. El cálculo del valor de ++i
se secuencia después del efecto secundario de ++i
(la modificación de i
) (B)
Es por eso que i = ++i + 1
es seguro : hay una cadena de secuencias antes entre el cálculo del valor en (A) y el efecto secundario sobre la misma variable en (B).
(a) El Estándar define ++expr
en términos de expr += 1
, que se define como expr = expr + 1
con expr
siendo evaluado solo una vez.
Para este expr = expr + 1
, por lo tanto, tenemos un solo cálculo de valor de expr
. El efecto secundario de =
se secuencia antes del cálculo del valor de todo expr = expr + 1
, y se secuencia después del cálculo del valor de los operandos expr
(LHS) y expr + 1
(RHS).
Esto corresponde a mi afirmación de que para ++expr
, el efecto secundario se secuencia antes del cálculo del valor de ++expr
.
Acerca de i += ++i + 1
¿El cálculo de valor de
i += ++i + 1
implica un comportamiento indefinido?Dado que el LHS de
+=
sigue siendo un valor l (y su RHS sigue siendo un valor prve), se aplica el mismo razonamiento anterior en cuanto a (1) y (2); en cuanto a (3) el cálculo del valor del operador+=
ahora ambos deben obtener el valor actual dei
, y luego (obviamente secuenciado después de él, incluso si el estándar no lo dice explícitamente, o de lo contrario la ejecución de dichos operadores siempre invocar un comportamiento indefinido) realice la adición del RHS y almacene el resultado eni
.
Creo que este es el problema: la adición de i
en el LHS de i +=
al resultado de ++i + 1
requiere conocer el valor de i
- un cálculo de valor (que puede significar cargar el valor de i
). Este cálculo del valor no se ha seguido con respecto a la modificación realizada por ++i
. Esto es esencialmente lo que dices en tu descripción alternativa, siguiendo la reescritura ordenada por el Estándar i += expr
-> i = i + expr
. Aquí, el cálculo del valor de i
dentro de i + expr
se realiza con respecto al cálculo del valor de expr
. Ahí es donde obtienes UB .
Tenga en cuenta que un cálculo de valor puede tener dos resultados: la "dirección" de un objeto o el valor de un objeto. En una expresión i = 42
, el cálculo del valor de lhs "produce la dirección" de i
; es decir, el compilador necesita averiguar dónde almacenar los rhs (bajo las reglas de comportamiento observable de la máquina abstracta). En una expresión i + 42
, el cálculo de valor de i
produce el valor. En el párrafo anterior, me refería al segundo tipo, por lo tanto [intro.execution] p15 se aplica:
Si un efecto secundario en un objeto escalar no se está secuenciando en relación con otro efecto secundario en el mismo objeto escalar o con un cálculo de valor que utiliza el valor del mismo objeto escalar, el comportamiento no está definido.
Otro enfoque para i += ++i + 1
el cálculo del valor del operador
+=
ahora debe obtener el valor actual dei
y luego realizar la adición del RHS
El RHS es ++i + 1
. La computación del resultado de esta expresión (el cálculo del valor) no se realiza con respecto al cálculo del valor de i
del LHS. Entonces, la palabra que aparece en esta oración es engañosa: por supuesto, primero debe cargar i
y luego agregarle el resultado de la RHS. Pero no hay un orden entre el efecto secundario del RHS y el cálculo del valor para obtener el valor del LHS. Por ejemplo, podría obtener para el LHS el valor antiguo o nuevo de i
, modificado por el RHS.
En general, una tienda y una carga "simultánea" es una carrera de datos, que conduce a un comportamiento indefinido.
Dirigiéndose a la adición
usando un fictivo
|||
operador para designar evaluaciones no secuenciadas, uno podría tratar de definirE op= F;
(con operandos int por simplicidad) como equivalente a{ int& L=E ||| int R=F; L = L + R; }
{ int& L=E ||| int R=F; L = L + R; }
{ int& L=E ||| int R=F; L = L + R; }
, pero luego el ejemplo ya no tiene UB.
Deje que E
sea i
y F
sea ++i
(no necesitamos el + 1
). Entonces, para i = ++i
int* lhs_address;
int lhs_value;
int* rhs_address;
int rhs_value;
( lhs_address = &i)
||| (i = i+1, rhs_address = &i, rhs_value = *rhs_address);
*lhs_address = rhs_value;
Por otro lado, para i += ++i
( lhs_address = &i, lhs_value = *lhs_address)
||| (i = i+1, rhs_address = &i, rhs_value = *rhs_address);
int total_value = lhs_value + rhs_value;
*lhs_address = total_value;
Esto pretende representar mi comprensión de las garantías de secuencia. Tenga en cuenta que el operador secuencia todos los cómputos de valores y efectos secundarios del LHS antes que los del RHS. Los paréntesis no afectan la secuencia. En el segundo caso, i += ++i
, tenemos una modificación de i
secuenciar una conversión lvalue-a -valor de i
=> UB.
El estándar no trata las asignaciones de compuestos como primitivas de segunda clase para las cuales no es necesaria una definición de semántica por separado.
Yo diría que es una redundancia. La reescritura de E1 op = E2
a E1 = E1 op E2
también incluye qué tipos de expresión y categorías de valores son necesarios (en el rhs, 5.17 / 1 dice algo sobre lhs), qué sucede con los tipos de puntero, las conversiones requeridas, etc. Lo triste es que la frase sobre "Con respecto a un ..." en 5.17 / 1 no está en 5.17 / 7 como una excepción de esa equivalencia.
De cualquier manera, creo que deberíamos comparar las garantías y los requisitos para la asignación compuesta versus la asignación simple más el operador, y ver si hay alguna contradicción.
Una vez que ponemos eso "Con respecto a un ..." también en la lista de excepciones en 5.17 / 7, no creo que haya una contradicción.
Resulta que, como puede ver en la discusión de la respuesta de Marc van Leeuwen, esta oración lleva a la siguiente observación interesante:
int i; // global
int& f() { return ++i; }
int main() {
i = i + f(); // (A)
i += f(); // (B)
}
Parece que (A) tiene dos resultados posibles, ya que la evaluación del cuerpo de f
está indeterminadamente secuenciada con el cálculo del valor de i
en i + f()
.
En (B), por otro lado, la evaluación del cuerpo de f()
se secuencia antes del cálculo de valor de i
, ya que +=
debe verse como una operación única, y f()
ciertamente debe evaluarse antes de la asignación de +=
.
Desde la perspectiva del escritor del compilador, que no se preocupan "i += ++i + 1"
, porque lo que el compilador lo hace, el programador no puede obtener el resultado correcto, pero sin duda lo que se merecen. Y nadie escribe código como eso. Lo que el escritor del compilador le importa es
*p += ++(*q) + 1;
El código debe leer *p
y *q
, aumentar *q
en un 1, y aumentar *p
en cierta cantidad que se calcula. Aquí el escritor del compilador se preocupa por restricciones en el orden de lectura y escritura. Obviamente, si P y el punto Q a diferentes objetos, la orden no hace ninguna diferencia, pero si p == q
entonces se hará una diferencia. Una vez más, p
será diferente de q
menos que el programador escribir el código es una locura.
Al hacer que el código no definido, el lenguaje permite al compilador para producir el código más rápido posible sin el cuidado de los programadores dementes. Al hacer que el código definido, el lenguaje obliga al compilador para producir código que se ajusta al estándar, incluso en casos de locura, que pueden hacer que se ejecute más lento. Ambos autores de compiladores y programadores sanas no les gusta eso.
Así que incluso si el comportamiento se define en C ++ 11, sería muy peligroso usarlo, debido a que (a) un compilador no puede ser cambiado de comportamiento C ++ 03, y (b) puede ser un comportamiento indefinido en C ++ 14, por las razones anteriores.