Quais são as garantias de ordem de avaliação introduzidas pelo C++ 17?




c++17 operator-precedence (2)

A intercalação é proibida em C ++ 17

Em C ++ 14, o seguinte não era seguro:

void foo(std::unique_ptr<A>, std::unique_ptr<B> );

foo(std::unique_ptr<A>(new A), std::unique_ptr<B>(new B));

Existem quatro operações que acontecem aqui durante a chamada de função

  1. new A
  2. construtor unique_ptr<A>
  3. new B
  4. construtor unique_ptr<B>

A ordenação destes foi completamente não especificada, e assim um ordenamento perfeitamente válido é (1), (3), (2), (4). Se esta ordenação foi selecionada e (3) lançada, então a memória de (1) vaza - ainda não rodamos (2), o que teria evitado o vazamento.

Em C ++ 17, as novas regras proíbem a intercalação. De [intro.execution]:

Para cada invocação de função F, para cada avaliação A que ocorre dentro de F e toda avaliação B que não ocorre dentro de F mas é avaliada no mesmo thread e como parte do mesmo manipulador de sinal (se houver), A é seqüenciado antes de B ou B é sequenciado antes de A.

Há uma nota de rodapé nessa frase que diz:

Em outras palavras, as execuções de função não se intercalam entre si.

Isso nos deixa com duas ordenações válidas: (1), (2), (3), (4) ou (3), (4), (1), (2). Não é especificado qual pedido é feito, mas ambos são seguros. Todos os pedidos em que (1) (3) ocorrem antes (2) e (4) são proibidos.

Quais são as implicações das garantias de ordem de avaliação votadas em C ++ 17 (P0145) no código C ++ típico?

O que isso muda sobre coisas como

i=1;
f(i++, i)

e

std::cout << f() << f() << f() ;

ou

f(g(),h(),j());

Alguns casos comuns em que a ordem de avaliação não foi especificada até agora, são especificados e válidos com o C++17 . Algum comportamento indefinido agora não é especificado.

E sobre coisas como

i=1;
f(i++, i)

foi indefinido, mas agora não é especificado. Especificamente, o que não é especificado é a ordem na qual cada argumento para f é avaliado em relação aos outros. i++ pode ser avaliado antes de i ou vice-versa. De fato, pode avaliar uma segunda chamada em uma ordem diferente, apesar de estar sob o mesmo compilador.

No entanto, a avaliação de cada argumento é necessária para executar completamente, com todos os efeitos colaterais, antes da execução de qualquer outro argumento. Então você pode obter f(1, 1) (segundo argumento avaliado primeiro) ou f(1, 2) (primeiro argumento avaliado primeiro). Mas você nunca obterá f(2, 2) ou qualquer outra coisa dessa natureza.

std::cout << f() << f() << f() ;

Não foi especificado, mas será compatível com a precedência do operador, de modo que a primeira avaliação de f virá primeiro no fluxo. (exemplos abaixo).

f(g(),h(),j());

ainda tem uma ordem de avaliação não especificada de g, h, j. Note que para getf()(g(),h(),j()) , o estado das regras que getf() será avaliado antes de g,h,j .

Observe também o seguinte exemplo do texto da proposta:

 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, "");

O exemplo vem da linguagem de programação C ++, 4 ª edição, Stroustrup e costumava ser um comportamento não especificado, mas com C + + 17 funcionará como esperado. Houve problemas semelhantes com funções recuperáveis ​​( .then( . . . ) ).

Como outro exemplo, considere o seguinte:

#include <iostream>
#include <string>
#include <vector>
#include <cassert>

struct Speaker{
    int i =0;
    Speaker(std::vector<std::string> words) :words(words) {}
    std::vector<std::string> words;
    std::string operator()(){
        assert(words.size()>0);
        if(i==words.size()) i=0;
        // pre- C++17 version:
        auto word = words[i] + (i+1==words.size()?"\n":",");
        ++i;
        return word;
        // Still not possible with C++17:
        // return words[i++] + (i==words.size()?"\n":",");

    }   
};

int main() {
    auto spk = Speaker{{"All", "Work", "and", "no", "play"}};
    std::cout << spk() << spk() << spk() << spk() << spk() ;
}

Com o C ++ 14 e antes podemos (e vamos) obter resultados como

play
no,and,Work,All,

ao invés de

All,work,and,no,play

Note que o acima é de fato o mesmo que

(((((std::cout << spk()) << spk()) << spk()) << spk()) << spk()) ;

Mas ainda assim, antes do C ++ 17, não havia garantia de que as primeiras chamadas viriam primeiro para o fluxo.

Referências: Da proposta aceita :

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 mudar de operador são avaliados da esquerda para a direita. Em resumo, as seguintes expressões são avaliadas na ordem a, depois b, depois c, depois d:

  1. ab
  2. a-> b
  3. a -> * b
  4. a (b1, b2, b3)
  5. b @ = a
  6. a [b]
  7. a << b
  8. a >> b

Além disso, sugerimos a seguinte regra adicional: a ordem de avaliação de uma expressão envolvendo um operador sobrecarregado é determinada pela ordem associada ao operador interno correspondente, não pelas regras para chamadas de função.

Editar nota: Minha resposta original interpretou erroneamente a(b1, b2, b3) . A ordem de b1 , b2 , b3 ainda não é especificada. (obrigado @KABoissonneault, todos os comentaristas)

No entanto, (como aponta Yakk) e isso é importante: Mesmo quando b1 , b2 , b3 são expressões não-triviais, cada uma delas é completamente avaliada e vinculada ao respectivo parâmetro de função antes que os outros sejam iniciados para serem avaliados. O padrão afirma isso assim:

§5.2.2 - Chamada de função 5.2.2.4:

. . . A expressão postfix é sequenciada antes de cada expressão na lista de expressão e em qualquer argumento padrão. Cada cálculo de valor e efeito colateral associado à inicialização de um parâmetro, e a própria inicialização, são sequenciados antes de cada cálculo de valor e efeito colateral associado à inicialização de qualquer parâmetro subseqüente.

No entanto, uma dessas novas frases está faltando no rascunho do github :

Cada cálculo de valor e efeito colateral associado à inicialização de um parâmetro, e a própria inicialização, são sequenciados antes de cada cálculo de valor e efeito colateral associado à inicialização de qualquer parâmetro subseqüente.

O exemplo está aí. Ele resolve um problema de décadas ( como explicado por Herb Sutter ) com exceção de segurança onde coisas como

f(std::unique_ptr<A> a, std::unique_ptr<B> b);

f(get_raw_a(),get_raw_a()); 

get_raw_a() se uma das chamadas get_raw_a() fosse get_raw_a() antes que o outro ponteiro bruto fosse vinculado ao parâmetro de ponteiro inteligente. edit: como apontado por TC, o exemplo é falho, pois a construção unique_ptr do ponteiro bruto é explícita, evitando que isso seja compilado.

Observe também essa question clássica (com a tag C , não C ++ ):

int x=0;
x++ + ++x;

ainda está indefinido.





operator-precedence