Este código da seção 36.3.6 da 4ª edição da “Linguagem de Programação C ++” tem um comportamento bem definido?

94

Na seção de Operações semelhantes a STL da The C ++ Programming Language 4ª edição de Bjarne Stroustrup, o código a seguir é usado como um exemplo de encadeamento :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" ) ;
}

A declaração falha em gcc( veja ao vivo ) e Visual Studio( veja ao vivo ), mas não falha ao usar o Clang ( veja ao vivo ).

Por que estou obtendo resultados diferentes? Algum desses compiladores está avaliando incorretamente a expressão de encadeamento ou este código exibe alguma forma de comportamento não especificado ou indefinido ?

Shafik Yaghmour
fonte
Melhor:s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
Ben Voigt
20
bug à parte, eu sou o único que acha que um código feio como esse não deveria estar no livro?
Karoly Horvath
5
@KarolyHorvath Observe que cout << a << b << coperator<<(operator<<(operator<<(cout, a), b), c)é apenas um pouco menos feio.
Oktalist 01 de
1
@Oktalist: :) pelo menos eu entendi a intenção. ensina pesquisa de nome dependente de argumento e sintaxe de operador ao mesmo tempo em um formato conciso ... e não dá a impressão de que você deveria realmente escrever um código como esse.
Karoly Horvath

Respostas:

104

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 replacedeve 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 findchamadas podem ser avaliadas antes ou depois de replace, o que é importante, pois tem um efeito colateral sem de uma forma que alteraria o resultado de find, ele altera a duração de s. Portanto, dependendo de quando isso replacefor avaliado em relação às duas findchamadas, 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 4e 7pode ser dividido em mais subexpressões. Assim:

  • Aé sequenciado antes do Bqual é sequenciado antes do Cqual é sequenciado antesD
  • 1a 9são sequenciados indeterminadamente em relação a outras subexpressões com algumas das exceções listadas abaixo
    • 1para 3serem sequenciados antesB
    • 4para 6serem sequenciados antesC
    • 7para 9serem sequenciados antesD

A chave para este problema é que:

  • 4a 9serem sequenciados indeterminadamente em relação aB

A ordem potencial de escolha da avaliação para 4e 7com respeito a Bexplica a diferença nos resultados entre clange gccdurante a avaliação f2(). Em meus testes clangavalia Bantes de avaliar 4e 7enquanto gccavalia 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]

Shafik Yaghmour
fonte
7
Estou um pouco surpreso ao ver que "muitos especialistas" ignoraram o problema, é sabido que a avaliação da expressão pós-fixada de uma chamada de função não é sequenciada antes de avaliar os argumentos (em todas as versões de C e C ++).
MM
@ShafikYaghmour As chamadas de função são sequenciadas indeterminadamente em relação umas às outras e a tudo o mais, com exceção dos relacionamentos sequenciados antes que você anotou. No entanto, a avaliação de 1, 2, 3, 5, 6, 8, 9 "even", "don't"e as várias instâncias de não ssão sequenciadas entre si.
TC
4
@TC não, não é (que é como esse "bug" surge). Por exemplo foo().func( bar() ), ele pode ligar foo()antes ou depois de ligar bar(). A expressão pós-fixada é foo().func. Os argumentos e a expressão pós-fixada são sequenciados antes do corpo de func(), mas não sequenciados em relação um ao outro.
MM
@MattMcNabb Ah, certo, eu li errado. Você está falando sobre a expressão postfix em si, e não sobre a chamada. Sim, isso mesmo, eles não são sequenciados (a menos que alguma outra regra se aplique, é claro).
TC
6
Há também o fator de que se tende a assumir que o código que aparece em um livro de B.Stroustrup está correto, caso contrário, alguém certamente já teria notado! (relacionado; os usuários do SO ainda encontram novos erros em K&R)
MM
4

Pretende-se acrescentar informações sobre o assunto em relação ao C ++ 17. A proposta ( Refining Expression Evaluation Order for Idimatic C ++ Revisão 2 ) para C++17abordar o problema citando o código acima foi como amostra.

Como sugerido, adicionei informações relevantes da proposta e para citar (destaques a minha):

A ordem de avaliação da expressão, conforme especificado atualmente no padrão, prejudica os conselhos, os idiomas de programação populares ou a segurança relativa das instalações da biblioteca padrão. As armadilhas não são apenas para novatos ou programadores descuidados. Eles afetam a todos nós indiscriminadamente, mesmo quando conhecemos as regras.

Considere o seguinte 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");
}

A asserção deve validar o resultado pretendido pelo programador. Ele usa o "encadeamento" de chamadas de função de membro, uma prática padrão comum. Este código foi revisado por especialistas em C ++ em todo o mundo e publicado (The C ++ Programming Language, 4ª edição.) No entanto, sua vulnerabilidade a uma ordem de avaliação não especificada foi descoberta apenas recentemente por uma ferramenta.

O artigo sugeriu mudar a C++17regra de avaliação da ordem de expressão que foi influenciada por Ce existe há mais de três décadas. Ele propunha que a linguagem deveria garantir expressões contemporâneas ou correr o risco de "armadilhas e fontes de bugs obscuros, difíceis de encontrar" , como o que aconteceu com o espécime de código acima.

A proposta C++17é exigir que toda expressão tenha uma ordem de avaliação bem definida :

  • As expressões Postfix são avaliadas da esquerda para a direita. Isso inclui chamadas de funções e expressões de seleção de membros.
  • As expressões de atribuição são avaliadas da direita para a esquerda. Isso inclui atribuições compostas.
  • Operandos para operadores de deslocamento são avaliados da esquerda para a direita.
  • A ordem de avaliação de uma expressão envolvendo um operador sobrecarregado é determinada pela ordem associada ao operador integrado correspondente, não pelas regras para chamadas de função.

O código acima é compilado com sucesso usando GCC 7.1.1e Clang 4.0.0.

ricky m
fonte