c++ c++11 language-lawyer order-of-evaluation operator-precedence unspecified-behavior

¿Este código del "Lenguaje de programación C++", 4a edición, sección 36.3.6, tiene un comportamiento bien definido?



c++11 language-lawyer (2)

El código exhibe un comportamiento no especificado debido al orden no especificado de evaluación de las sub-expresiones, aunque no invoca un comportamiento indefinido ya que todos los efectos secundarios se realizan dentro de funciones que introducen una relación de secuencia entre los efectos secundarios en este caso.

Este ejemplo se menciona en la propuesta N4228: Orden de evaluación de expresión de refinación para Idiomatic C ++ que dice lo siguiente sobre el código en la pregunta:

[...] Este código ha sido revisado por expertos de C ++ en todo el mundo y publicado (The C ++ Programming Language, edición). Sin embargo, su vulnerabilidad al orden de evaluación no especificado ha sido descubierta recientemente por una herramienta [.. .]

Detalles

Puede ser obvio para muchos que los argumentos de las funciones tienen un orden de evaluación no especificado, pero probablemente no sea tan obvio cómo interactúa este comportamiento con las llamadas de funciones encadenadas. No fue obvio para mí cuando analicé este caso por primera vez y aparentemente tampoco para todos los revisores expertos .

A primera vista, puede parecer que, dado que cada replace debe evaluarse de izquierda a derecha, los grupos de argumentos de función correspondientes también deben evaluarse como grupos de izquierda a derecha.

Esto es incorrecto, los argumentos de función tienen un orden de evaluación no especificado, aunque el encadenamiento de llamadas de función introduce un orden de evaluación de izquierda a derecha para cada llamada de función, los argumentos de cada llamada de función solo se secuencian antes con respecto a la llamada de función miembro de la que forman parte de. En particular, esto afecta las siguientes llamadas:

s.find( "even" )

y:

s.find( " don''t" )

que se secuencian indeterminadamente con respecto a:

s.replace(0, 4, "" )

las dos llamadas de find podrían evaluarse antes o después del replace , lo cual es importante ya que tiene un efecto secundario en s de una manera que alteraría el resultado de la find , cambia la longitud de s . Entonces, dependiendo de cuándo se evalúa ese replace relación con las dos llamadas de find , el resultado será diferente.

Si observamos la expresión de encadenamiento y examinamos el orden de evaluación de algunas de las subexpresiones:

s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" ) ^ ^ ^ ^ ^ ^ ^ ^ ^ A B | | | C | | | 1 2 3 4 5 6

y:

.replace( s.find( " don''t" ), 6, "" ); ^ ^ ^ ^ D | | | 7 8 9

Tenga en cuenta que estamos ignorando el hecho de que 4 y 7 pueden desglosarse aún más en más expresiones secundarias. Entonces:

  • A se secuencia antes de B que se secuencia antes de C que se secuencia antes de D
  • 1 a 9 se secuencian indeterminadamente con respecto a otras subexpresiones con algunas de las excepciones enumeradas a continuación
    • 1 a 3 se secuencian antes de B
    • 4 a 6 se secuencian antes de C
    • 7 a 9 se secuencian antes de D

La clave de este problema es que:

  • 4 a 9 se secuencian indeterminadamente con respecto a B

El orden potencial de elección de evaluación para 4 y 7 con respecto a B explica la diferencia en los resultados entre clang y gcc al evaluar f2() . En mis pruebas, clang evalúa B antes de evaluar 4 y 7 mientras que gcc evalúa después. Podemos usar el siguiente programa de prueba para demostrar lo que sucede en cada caso:

#include <iostream> #include <string> std::string::size_type my_find( std::string s, const char *cs ) { std::string::size_type pos = s.find( cs ) ; std::cout << "position " << cs << " found in complete expression: " << pos << std::endl ; return pos ; } int main() { std::string s = "but I have heard it works even if you don''t believe in it" ; std::string copy_s = s ; std::cout << "position of even before s.replace(0, 4, /"/" ): " << s.find( "even" ) << std::endl ; std::cout << "position of don''t before s.replace(0, 4, /"/" ): " << s.find( " don''t" ) << std::endl << std::endl; copy_s.replace(0, 4, "" ) ; std::cout << "position of even after s.replace(0, 4, /"/" ): " << copy_s.find( "even" ) << std::endl ; std::cout << "position of don''t after s.replace(0, 4, /"/" ): " << copy_s.find( " don''t" ) << std::endl << std::endl; s.replace(0, 4, "" ).replace( my_find( s, "even" ) , 4, "only" ) .replace( my_find( s, " don''t" ), 6, "" ); std::cout << "Result: " << s << std::endl ; }

Resultado para gcc ( verlo en vivo )

position of even before s.replace(0, 4, "" ): 26 position of don''t before s.replace(0, 4, "" ): 37 position of even after s.replace(0, 4, "" ): 22 position of don''t after s.replace(0, 4, "" ): 33 position don''t found in complete expression: 37 position even found in complete expression: 26 Result: I have heard it works evenonlyyou donieve in it

Resultado para el clang ( verlo en vivo ):

position of even before s.replace(0, 4, "" ): 26 position of don''t before s.replace(0, 4, "" ): 37 position of even after s.replace(0, 4, "" ): 22 position of don''t after s.replace(0, 4, "" ): 33 position even found in complete expression: 22 position don''t found in complete expression: 33 Result: I have heard it works only if you believe in it

Resultado para Visual Studio ( verlo en vivo ):

position of even before s.replace(0, 4, "" ): 26 position of don''t before s.replace(0, 4, "" ): 37 position of even after s.replace(0, 4, "" ): 22 position of don''t after s.replace(0, 4, "" ): 33 position don''t found in complete expression: 37 position even found in complete expression: 26 Result: I have heard it works evenonlyyou donieve in it

Detalles del estándar

Sabemos que, a menos que se especifique que las evaluaciones de las subexpresiones no están secuenciadas, esto es del borrador de C ++ 11 estándar sección 1.9 Ejecución del programa que dice:

Excepto donde se indique, las evaluaciones de operandos de operadores individuales y de subexpresiones de expresiones individuales no tienen secuencia [...]

y sabemos que una llamada a la función introduce una secuencia secuencial antes de que la relación de la función llame a la expresión y los argumentos de postfix con respecto al cuerpo de la función, de la sección 1.9 :

[...] Cuando se llama a una función (ya sea que la función esté en línea o no), cada cálculo de valor y efecto secundario asociado con cualquier expresión de argumento, o con la expresión de postfijo 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. [...]

También sabemos que el acceso de los miembros de la clase y, por lo tanto, el encadenamiento se evaluarán de izquierda a derecha, desde la sección 5.2.5 Acceso de los miembros de la clase que dice:

[...] La expresión de postfix antes de evaluar el punto o la flecha; 64 el resultado de esa evaluación, junto con la expresión id, determina el resultado de toda la expresión postfix.

Tenga en cuenta que, en el caso de que la expresión id termine siendo una función miembro no estática, no especifica el orden de evaluación de la lista de expresiones dentro de () ya que es una subexpresión separada. La gramática relevante de las expresiones 5.2 Postfix :

postfix-expression: postfix-expression ( expression-listopt) // function call postfix-expression . templateopt id-expression // Class member access, ends // up as a postfix-expression

C ++ 17 cambios

La propuesta p0145r3: Orden de evaluación de expresión de refinación para Idiomatic C ++ hizo varios cambios. Incluyendo cambios que le dan al código un comportamiento bien especificado al fortalecer el orden de las reglas de evaluación para las expresiones postfix y su lista de expresiones .

[expr.call]p5 dice:

La expresión postfix se secuencia antes de cada expresión en la lista de expresiones y cualquier argumento predeterminado . La inicialización de un parámetro, que incluye cada cálculo de valor asociado y efecto secundario, se secuencia indeterminadamente con respecto a cualquier otro parámetro. [Nota: Todos los efectos secundarios de las evaluaciones de argumentos se secuencian antes de ingresar la función (ver 4.6). —Final nota] [Ejemplo:

void f() { std::string s = "but I have heard it works even if you don’t believe in it"; s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" don’t"), 6, ""); assert(s == "I have heard it works only if you believe in it"); // OK }

—Ejemplo]

En El lenguaje de programación C ++ de Bjarne Stroustrup, 4.a edición, sección 36.3.6 Operaciones similares a STL, el siguiente código se utiliza como ejemplo de chaining :

void f2() { std::string s = "but I have heard it works even if you don''t believe in it" ; s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" ) .replace( s.find( " don''t" ), 6, "" ); assert( s == "I have heard it works only if you believe in it" ) ; }

La afirmación falla en gcc ( verlo en vivo ) y Visual Studio ( verlo en vivo ), pero no falla cuando se usa Clang ( verlo en vivo ).

¿Por qué obtengo resultados diferentes? ¿Alguno de estos compiladores está evaluando incorrectamente la expresión de encadenamiento o este código exhibe alguna forma de comportamiento no unspecified o indefinido ?


Se pretende agregar información sobre el asunto con respecto a C ++ 17. La propuesta ( Orden de evaluación de expresión de refinación para la revisión idiomática de C ++ 2 ) para C++17 abordó el problema citando el código anterior como muestra.

Como sugerí, agregué información relevante de la propuesta y para citar (destaca la mía):

El orden de evaluación de la expresión, tal como se especifica actualmente en el estándar, socava los consejos, los modismos de programación populares o la relativa seguridad de las instalaciones estándar de la biblioteca. Las trampas no son solo para los principiantes o el programador descuidado. Nos afectan a todos indiscriminadamente, incluso cuando conocemos las reglas.

Considere el siguiente fragmento de programa:

void f() { std::string s = "but I have heard it works even if you don''t believe in it" s.replace(0, 4, "").replace(s.find("even"), 4, "only") .replace(s.find(" don''t"), 6, ""); assert(s == "I have heard it works only if you believe in it"); }

Se supone que la afirmación valida el resultado previsto del programador. Utiliza el "encadenamiento" de llamadas a funciones miembro, una práctica estándar común. Este código ha sido revisado por expertos de C ++ en todo el mundo y publicado (The C ++ Programming Language, cuarta edición). Sin embargo, su vulnerabilidad a un orden de evaluación no especificado ha sido descubierta recientemente por una herramienta.

El artículo sugirió cambiar la regla anterior a C++17 en el orden de evaluación de la expresión, que fue influenciada por C y que ha existido por más de tres décadas. Propuso que el lenguaje debería garantizar modismos contemporáneos o arriesgarse a "trampas y fuentes de errores oscuros y difíciles de encontrar" , como lo que sucedió con el espécimen de código anterior.

La propuesta para C++17 es requerir que cada expresión tenga un orden de evaluación bien definido :

  • Las expresiones de postfix se evalúan de izquierda a derecha. Esto incluye llamadas a funciones y expresiones de selección de miembros.
  • Las expresiones de asignación se evalúan de derecha a izquierda. Esto incluye asignaciones compuestas.
  • Los operandos para desplazar operadores se evalúan de izquierda a derecha.
  • El orden de evaluación de una expresión que involucra un operador sobrecargado está determinado por el orden asociado con el operador integrado correspondiente, no por las reglas para las llamadas a funciones.

El código anterior se compila correctamente con GCC 7.1.1 y Clang 4.0.0 .