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 find
llamadas podrían evaluarse antes o después de replace
, lo cual es importante, ya que tiene un efecto secundario de s
una manera que alteraría el resultado de find
, cambia la longitud de s
. Entonces, dependiendo de cuándo replace
se 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 4
y 7
se puede dividir en más sub-expresiones. Entonces:
A
se secuencia antes de B
que se secuencia antes de C
que se secuencia antesD
1
a 9
tienen una secuencia indeterminada con respecto a otras sub-expresiones con algunas de las excepciones que se enumeran a continuación
1
a 3
se secuencian antesB
4
a 6
se secuencian antesC
7
a 9
se secuencian antesD
La clave de este problema es que:
4
a 9
están secuenciados indeterminadamente con respecto aB
El orden potencial de elección de la 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
while gcc
evalú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, "" );