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

95

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 em coisas como as seguintes?

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

e

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

ou

f(g(), h(), j());
Johan Lundberg
fonte
Relacionado à ordem de avaliação da instrução de atribuição em C ++ e Este código da seção 36.3.6 da 4ª edição “The C ++ Programming Language” tem um comportamento bem definido? ambos cobertos pelo artigo. O primeiro pode ser um bom exemplo adicional em sua resposta abaixo.
Shafik Yaghmour

Respostas:

83

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

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

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

No entanto, a avaliação de cada argumento é necessária para ser executada completamente, com todos os efeitos colaterais, antes da execução de qualquer outro argumento. Portanto, 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 se tornará compatível com a precedência do operador, de modo que a primeira avaliação de fvenha primeiro no fluxo (exemplos abaixo).

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

ainda tem uma ordem de avaliação não especificada de g, h e j. Observe que para getf()(g(),h(),j()), as regras indicam que getf()será avaliado antes 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 de The C ++ Programming Language , 4ª edição, Stroustrup, e costumava ser um comportamento não especificado, mas com C ++ 17 funcionará conforme o esperado. Ocorreram problemas semelhantes com funções retomá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 C ++ 14 e antes, podemos (e iremos) obter resultados como

play
no,and,Work,All,

ao invés de

All,work,and,no,play

Observe que o acima está em vigor o mesmo que

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

Mesmo assim, antes do C ++ 17 não havia garantia de que as primeiras chamadas viriam primeiro no 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 operadores de deslocamento são avaliados da esquerda para a direita. Em resumo, as seguintes expressões são avaliadas na ordem a, depois b, depois c e 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 embutido correspondente, não pelas regras para chamadas de função.

Editar nota: minha resposta original foi mal interpretada a(b1, b2, b3). A ordem de b1, b2, b3ainda é indeterminado. (obrigado @KABoissonneault, todos os comentaristas.)

No entanto, (como aponta @Yakk out) e isto é importante: Mesmo quando b1, b2, b3são expressões não-triviais, cada um deles são completamente avaliadas e amarrado ao respectivo parâmetro de função antes que os outros são começou a ser avaliada. O padrão afirma 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ões e 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, é sequenciado antes de cada cálculo de valor e efeito colateral associado à inicialização de qualquer parâmetro subsequente.

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, é sequenciado antes de cada cálculo de valor e efeito colateral associado à inicialização de qualquer parâmetro subsequente.

O exemplo está aí. Ele resolve problemas de décadas atrás ( conforme explicado por Herb Sutter ) com segurança excepcional, onde coisas como

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

f(get_raw_a(), get_raw_a());

vazaria se uma das chamadas get_raw_a()fosse lançada antes que o outro ponteiro bruto fosse vinculado ao seu parâmetro de ponteiro inteligente.

Conforme apontado por TC, o exemplo é falho, pois a construção de unique_ptr a partir do ponteiro bruto é explícita, impedindo que ele seja compilado. *

Observe também esta pergunta clássica (marcada como C , não C ++ ):

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

ainda está indefinido.

Johan Lundberg
fonte
1
"Uma segunda proposta subsidiária substitui a ordem de avaliação das chamadas de função da seguinte maneira: a função é avaliada antes de todos os seus argumentos, mas qualquer par de argumentos (da lista de argumentos) é sequenciado indeterminadamente; o que significa que um é avaliado antes do outro, exceto o a ordem não é especificada; é garantido que a função é avaliada antes dos argumentos. Isso reflete uma sugestão feita por alguns membros do Grupo de Trabalho Principal. "
Yakk - Adam Nevraumont
1
Estou recebendo essa impressão do jornal dizendo que "as seguintes expressões são avaliadas na ordem a, então b, então c, então d" e, em seguida a(b1, b2, b3), mostrando , sugerindo que todas as bexpressões não são necessariamente avaliadas em qualquer ordem (caso contrário, seria a(b, c, d))
KABoissonneault
1
@KABoissoneault, Você está correto e eu atualizei a resposta de acordo. Além disso, todas: as citações são da versão 3, que é a versão votada, tanto quanto eu entendo.
Johan Lundberg
2
@JohanLundberg Há outra coisa no jornal que acredito ser importante. a(b1()(), b2()())pode encomendar b1()()e b2()()em qualquer ordem, mas não pode fazer b1(), em seguida, b2()()então b1()(): já não intercalam suas execuções podem. Em suma, "8. ORDEM DE AVALIAÇÃO ALTERNATIVA PARA CHAMADAS DE FUNÇÃO" fazia parte da alteração aprovada.
Yakk - Adam Nevraumont
3
f(i++, i)estava indefinido. Agora não foi especificado. O exemplo da string de Stroustrup provavelmente não foi especificado, não foi definido. `f (get_raw_a (), get_raw_a ());` não compilará já que o unique_ptrconstrutor relevante é explícito. Finalmente, x++ + ++xé indefinido, ponto final.
TC
44

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

No 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. unique_ptr<A> construtor
  3. new B
  4. unique_ptr<B> construtor

A ordenação destes não foi especificada e, portanto, uma ordenação perfeitamente válida é (1), (3), (2), (4). Se esta ordem foi selecionada e (3) joga, então a memória de (1) vaza - nós não rodamos (2) ainda, o que teria evitado o vazamento.


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

Para cada chamada de função F, para cada avaliação A que ocorre dentro de F e cada avaliação B que não ocorre dentro de F, mas é avaliada no mesmo encadeamento e como parte do mesmo manipulador de sinal (se houver), A é sequenciado 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ções não se intercalam.

Isso nos deixa com duas ordens válidas: (1), (2), (3), (4) ou (3), (4), (1), (2). Não é especificado qual pedido é usado, mas ambos são seguros. Todos os pedidos onde (1) (3) ambos acontecem antes de (2) e (4) agora são proibidos.

Barry
fonte
1
Um pouco à parte, mas esta foi uma das razões para boost :: make_shared, e posteriormente std :: make_shared (outra razão é menos alocações + melhor localidade). Parece que a motivação de segurança de exceção / vazamento de recursos não se aplica mais. Consulte o Exemplo de código 3, boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/… Editar e stackoverflow.com/a/48844115 , herbsutter.com/2013/05/29/gotw-89-solution- smart-pointers
Max Barraclough
3
Eu me pergunto como essa mudança afeta a otimização. O compilador agora reduziu drasticamente o número de opções de como combinar e intercalar as instruções da CPU relacionadas ao cálculo dos argumentos, portanto, isso pode levar a uma utilização mais pobre da CPU.
Violet Giraffe
2

Encontrei algumas notas sobre a ordem de avaliação da expressão:

  • P: Por que o c ++ não tem uma ordem especificada para avaliar os argumentos da função?

    Alguma ordem de avaliação garante operadores sobrecarregados e regras de argumento completo quando adicionadas em C ++ 17. Mas permanece aquele argumento que vai primeiro não é especificado. No C ++ 17, agora é especificado que a expressão que dá o que chamar (o código à esquerda do (da chamada de função) vai antes dos argumentos, e qualquer argumento avaliado primeiro é avaliado totalmente antes do próximo iniciado e, no caso de um método de objeto, o valor do objeto é avaliado antes dos argumentos do método.

  • Ordem de avaliação

    21) Cada expressão em uma lista de expressões separadas por vírgulas em um inicializador entre parênteses é avaliada como se fosse uma chamada de função ( sequenciada de forma indeterminada )

  • Expressões ambíguas

    A linguagem C ++ não garante a ordem em que os argumentos para uma chamada de função são avaliados.

Em P0145R3.Refining Expression Evaluation Order for Idiomático C ++ encontrei:

O cálculo do valor e o efeito colateral associado da expressão postfix são sequenciados antes daqueles das expressões na lista de expressões. As inicializações dos parâmetros declarados são sequenciadas indeterminadamente sem intercalação.

Mas não o encontrei no padrão, em vez disso, no padrão eu encontrei:

6.8.1.8 Execução sequencial [introdução.execução] Diz-se que uma expressão X é sequenciada antes de uma expressão Y se cada cálculo de valor e cada efeito secundário associado à expressão X for sequenciado antes de cada cálculo de valor e cada efeito secundário associado à expressão Y .

6.8.1.9 Execução sequencial [introdução.execução] Cada cálculo de valor e efeito colateral associado a uma expressão completa é sequenciado antes de cada cálculo de valor e efeito colateral associado à próxima expressão completa a ser avaliada.

7.6.19.1 Operador vírgula [expr.comma] Um par de expressões separadas por uma vírgula é avaliado da esquerda para a direita; ...

Portanto, comparei de acordo com o comportamento em três compiladores para 14 e 17 padrões. O código explorado é:

#include <iostream>

struct A
{
    A& addInt(int i)
    {
        std::cout << "add int: " << i << "\n";
        return *this;
    }

    A& addFloat(float i)
    {
        std::cout << "add float: " << i << "\n";
        return *this;
    }
};

int computeInt()
{
    std::cout << "compute int\n";
    return 0;
}

float computeFloat()
{
    std::cout << "compute float\n";
    return 1.0f;
}

void compute(float, int)
{
    std::cout << "compute\n";
}

int main()
{
    A a;
    a.addFloat(computeFloat()).addInt(computeInt());
    std::cout << "Function call:\n";
    compute(computeFloat(), computeInt());
}

Resultados (quanto mais consistente é o clang):

<style type="text/css">
  .tg {
    border-collapse: collapse;
    border-spacing: 0;
    border-color: #aaa;
  }
  
  .tg td {
    font-family: Arial, sans-serif;
    font-size: 14px;
    padding: 10px 5px;
    border-style: solid;
    border-width: 1px;
    overflow: hidden;
    word-break: normal;
    border-color: #aaa;
    color: #333;
    background-color: #fff;
  }
  
  .tg th {
    font-family: Arial, sans-serif;
    font-size: 14px;
    font-weight: normal;
    padding: 10px 5px;
    border-style: solid;
    border-width: 1px;
    overflow: hidden;
    word-break: normal;
    border-color: #aaa;
    color: #fff;
    background-color: #f38630;
  }
  
  .tg .tg-0pky {
    border-color: inherit;
    text-align: left;
    vertical-align: top
  }
  
  .tg .tg-fymr {
    font-weight: bold;
    border-color: inherit;
    text-align: left;
    vertical-align: top
  }
</style>
<table class="tg">
  <tr>
    <th class="tg-0pky"></th>
    <th class="tg-fymr">C++14</th>
    <th class="tg-fymr">C++17</th>
  </tr>
  <tr>
    <td class="tg-fymr"><br>gcc 9.0.1<br></td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
  </tr>
  <tr>
    <td class="tg-fymr">clang 9</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
  </tr>
  <tr>
    <td class="tg-fymr">msvs 2017</td>
    <td class="tg-0pky">compute int<br>compute float<br>add float: 1<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
  </tr>
</table>

lvccgd
fonte