O código exibe um comportamento não especificado devido à ordem não especificada de avaliação de subexpressões, embora não invoque um comportamento indefinido, pois todos os efeitos colaterais são feitos dentro de funções que introduzem uma relação de sequenciamento entre os efeitos colaterais neste caso.
Este exemplo é mencionado na proposta N4228: Refining Expression Evaluation Order for Idimatic C ++ que diz o seguinte sobre o código em questão:
[...] Este código foi revisado por especialistas em C ++ em todo o mundo e publicado (The C ++ Programming Language, 4 a edição.) No entanto, sua vulnerabilidade a uma ordem não especificada de avaliação foi descoberta apenas recentemente por uma ferramenta [.. .]
Detalhes
Pode ser óbvio para muitos que os argumentos para funções têm uma ordem de avaliação não especificada, mas provavelmente não é tão óbvio como esse comportamento interage com chamadas de funções encadeadas. Não era óbvio para mim quando analisei este caso pela primeira vez e, aparentemente, nem para todos os revisores especialistas .
À primeira vista pode parecer que, uma vez que cada um replace
deve ser avaliado da esquerda para a direita, os grupos de argumentos de função correspondentes também devem ser avaliados como grupos da esquerda para a direita.
Isso está incorreto, os argumentos da função têm uma ordem de avaliação não especificada, embora as chamadas de função de encadeamento introduzam uma ordem de avaliação da esquerda para a direita para cada chamada de função, os argumentos de cada chamada de função só são sequenciados antes em relação à chamada de função de membro da qual fazem parte do. Em particular, isso afeta as seguintes chamadas:
s.find( "even" )
e:
s.find( " don't" )
que são sequenciados indeterminadamente em relação a:
s.replace(0, 4, "" )
as duas find
chamadas podem ser avaliadas antes ou depois de replace
, o que é importante, pois tem um efeito colateral s
em de uma forma que alteraria o resultado de find
, ele altera a duração de s
. Portanto, dependendo de quando isso replace
for avaliado em relação às duas find
chamadas, o resultado será diferente.
Se olharmos para a expressão de encadeamento e examinarmos a ordem de avaliação de algumas das subexpressões:
s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
^ ^ ^ ^ ^ ^ ^ ^ ^
A B | | | C | | |
1 2 3 4 5 6
e:
.replace( s.find( " don't" ), 6, "" );
^ ^ ^ ^
D | | |
7 8 9
Observe que estamos ignorando o fato de que 4
e 7
pode ser dividido em mais subexpressões. Assim:
A
é sequenciado antes do B
qual é sequenciado antes do C
qual é sequenciado antesD
1
a 9
são sequenciados indeterminadamente em relação a outras subexpressões com algumas das exceções listadas abaixo
1
para 3
serem sequenciados antesB
4
para 6
serem sequenciados antesC
7
para 9
serem sequenciados antesD
A chave para este problema é que:
4
a 9
serem sequenciados indeterminadamente em relação aB
A ordem potencial de escolha da avaliação para 4
e 7
com respeito a B
explica a diferença nos resultados entre clang
e gcc
durante a avaliação f2()
. Em meus testes clang
avalia B
antes de avaliar 4
e 7
enquanto gcc
avalia depois. Podemos usar o seguinte programa de teste para demonstrar o que está acontecendo em 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
( veja ao 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
( veja ao 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
( veja ao 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
Detalhes do padrão
Sabemos que, a menos que especificado, as avaliações de subexpressões não são sequenciadas, isto é do esboço da seção padrão C ++ 11 1.9
Execução do programa que diz:
Exceto onde indicado, as avaliações de operandos de operadores individuais e de subexpressões de expressões individuais não são sequenciadas. [...]
e sabemos que uma chamada de função introduz um relacionamento sequenciado antes da expressão e argumentos pós-fixados das chamadas de função com relação ao corpo da função, da seção 1.9
:
[...] Ao chamar uma função (seja a função embutida ou não), todo cálculo de valor e efeito colateral associado a qualquer expressão de argumento, ou à expressão pós-fixada que designa a função chamada, é sequenciado antes da execução de cada expressão ou instrução no corpo da função chamada. [...]
Também sabemos que o acesso do membro da classe e, portanto, o encadeamento será avaliado da esquerda para a direita, na seção 5.2.5
Acesso do membro da classe que diz:
[...] A expressão pós-fixada antes do ponto ou seta ser avaliada; 64
o resultado dessa avaliação, junto com a expressão id, determina o resultado de toda a expressão pós-fixada.
Observe que, no caso em que a expressão id acaba sendo uma função-membro não estática, ela não especifica a ordem de avaliação da lista de expressões dentro de, ()
visto que é uma subexpressão separada. A gramática relevante das 5.2
expressões Postfix :
postfix-expression:
postfix-expression ( expression-listopt) // function call
postfix-expression . templateopt id-expression // Class member access, ends
// up as a postfix-expression
Mudanças C ++ 17
A proposta p0145r3: Refinando a ordem de avaliação de expressão para C ++ idiomático fez várias alterações. Incluindo mudanças que dão ao código um comportamento bem especificado, fortalecendo a ordem das regras de avaliação para expressões postfix e sua lista de expressões .
[expr.call] p5 diz:
A expressão postfix é sequenciada antes de cada expressão na lista de expressões e qualquer argumento padrão . A inicialização de um parâmetro, incluindo todo cálculo de valor associado e efeito colateral, é sequenciada indeterminadamente em relação a qualquer outro parâmetro. [Nota: Todos os efeitos colaterais das avaliações do argumento são sequenciados antes que a função seja inserida (ver 4.6). —Enviar nota] [Exemplo:
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
}
—End exemplo]
s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );