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, 4ª 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]
s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );