¿Este código de la sección 36.3.6 de la cuarta edición del lenguaje de programación C ++ tiene un comportamiento bien definido?

94

En la sección Operaciones similares a STL del lenguaje de programación C ++ de Bjarne Stroustrup, la cuarta edición, se utiliza el siguiente código como ejemplo de encadenamiento :36.3.6

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 aserció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 muestra alguna forma de comportamiento no especificado o indefinido ?

Shafik Yaghmour
fuente
Mejor:s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
Ben Voigt
20
Dejando a un lado el error, ¿soy el único que piensa que un código tan feo como ese no debería estar en el libro?
Karoly Horvath
5
@KarolyHorvath Tenga en cuenta que cout << a << b << coperator<<(operator<<(operator<<(cout, a), b), c)es solo un poco menos feo.
Oktalist
1
@Oktalist: :) al menos entiendo la intención allí. enseña la búsqueda de nombre dependiente del argumento y la sintaxis del operador al mismo tiempo en un formato conciso ... y no da la impresión de que realmente deba escribir un código como ese.
Karoly Horvath

Respuestas:

104

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

Este ejemplo se menciona en la propuesta N4228: Refining Expression Evaluation Order for Idiomatic C ++, que dice lo siguiente sobre el código de la pregunta:

[...] Este código ha sido revisado por expertos en C ++ de todo el mundo y publicado (The C ++ Programming Language, edición). Sin embargo, su vulnerabilidad a un 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 este comportamiento interactúa con las llamadas de funciones encadenadas. No fue obvio para mí cuando analicé este caso por primera vez y aparentemente no 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 convocatorias:

s.find( "even" )

y:

s.find( " don't" )

que están secuenciados indeterminadamente con respecto a:

s.replace(0, 4, "" )

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

Si miramos la expresión de encadenamiento y examinamos el orden de evaluación de algunas de las sub-expresiones:

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 4y 7se puede dividir en más sub-expresiones. Entonces:

  • Ase secuencia antes de Bque se secuencia antes de Cque se secuencia antesD
  • 1a 9tienen una secuencia indeterminada con respecto a otras sub-expresiones con algunas de las excepciones que se enumeran a continuación
    • 1a 3se secuencian antesB
    • 4a 6se secuencian antesC
    • 7a 9se secuencian antesD

La clave de este problema es que:

  • 4a 9están secuenciados indeterminadamente con respecto aB

El orden potencial de elección de la evaluación para 4y 7con respecto a Bexplica la diferencia en los resultados entre clangy gccal evaluar f2(). En mis pruebas clangevalúa Bantes de evaluar 4y 7while gccevalúa después. Podemos utilizar el siguiente programa de prueba para demostrar lo que está sucediendo 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 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, las evaluaciones de las subexpresiones no están secuenciadas, esto es del borrador de la sección estándar C ++ 11 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 están secuenciadas. [...]

y sabemos que una llamada de función introduce una relación secuenciada antes de las llamadas de función expresión y argumentos de postfijo con respecto al cuerpo de la función, de la sección 1.9:

[...] Al llamar 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 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. [...]

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

[...] Se evalúa la expresión de sufijo antes del 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 del sufijo.

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 la ()ya que es una subexpresión separada. La gramática relevante de las 5.2 expresiones 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: Refining Expression Evaluation Order for Idiomatic C ++ realizó 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 expresiones-postfijo y su lista de expresiones .

[expr.call] p5 dice:

La expresión-sufijo se secuencia antes de cada expresión en la lista de expresiones y cualquier argumento predeterminado . La inicialización de un parámetro, incluidos todos los cálculos de valor asociados y los efectos secundarios, tiene una secuencia indeterminada con respecto a la de 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). —End note] [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 final]

Shafik Yaghmour
fuente
7
Estoy un poco sorprendido de ver que "muchos expertos" pasaron por alto el problema, es bien sabido que la evaluación de la expresión postfija de una llamada a función no está secuenciada, antes de evaluar los argumentos (en todas las versiones de C y C ++).
MM
@ShafikYaghmour Las llamadas a las funciones están secuenciadas indeterminadamente entre sí y con todo lo demás, con la excepción de las relaciones secuenciadas antes que anotó. Sin embargo, la evaluación de 1, 2, 3, 5, 6, 8, 9 "even", "don't"y las diversas instancias de no sestán secuenciadas entre sí.
TC
4
@TC no, no lo es (que es como surge este "error"). Por ejemplo foo().func( bar() ), podría llamar foo()antes o después de llamar bar(). La expresión-postfijo es foo().func. Los argumentos y la expresión-postfijo están secuenciados antes del cuerpo de func(), pero no están secuenciados entre sí.
MM
@MattMcNabb Ah, cierto, leí mal. Estás hablando de la expresión de postfijo en sí en lugar de la llamada. Sí, es cierto, no están secuenciados (a menos que se aplique alguna otra regla, por supuesto).
TC
6
También existe el factor de que uno tiende a asumir que el código que aparece en un libro de B.Stroustrup es correcto, de lo contrario, ¡alguien seguramente ya lo habría notado! (relacionado; los usuarios de SO todavía encuentran nuevos errores en K&R)
MM
4

Esto tiene la intención de agregar información sobre el tema con respecto a C ++ 17. La propuesta ( Refining Expression Evaluation Order for Idiomatic C ++ Revision 2 ) paraC++17 abordar el problema citando el código anterior fue como muestra.

Como se sugirió, agregué información relevante de la propuesta y cito (destaca la mía):

El orden de evaluación de expresiones, como se especifica actualmente en el estándar, socava los consejos, los modismos de programación populares o la seguridad relativa de las instalaciones de la biblioteca estándar. Las trampas no son solo para principiantes o programadores descuidados. 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 aserción valida el resultado deseado por el programador. Utiliza el "encadenamiento" de llamadas a funciones miembro, una práctica estándar común. Este código ha sido revisado por expertos en C ++ de todo el mundo y publicado (The C ++ Programming Language, 4ª edición). Sin embargo, su vulnerabilidad a un orden de evaluación no especificado ha sido descubierta recientemente por una herramienta.

El documento sugirió cambiar la C++17regla previa sobre el orden de evaluación de las expresiones, que fue influenciada por Cy ha existido durante 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 C++17es exigir que cada expresión tenga un orden de evaluación bien definido :

  • Las expresiones de sufijo 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 cambiar a los operadores se evalúan de izquierda a derecha.
  • El orden de evaluación de una expresión que involucra a un operador sobrecargado está determinado por el orden asociado con el operador incorporado correspondiente, no por las reglas para las llamadas a funciones.

El código anterior se compila con éxito usando GCC 7.1.1y Clang 4.0.0.

ricky m
fuente