Jakie są gwarancje kolejności oceny wprowadzone przez C ++ 17?

96

Jakie są konsekwencje gwarancji kolejności oceny głosowanych w C ++ 17 (P0145) w typowym kodzie C ++?

Co to zmienia w następujących sytuacjach?

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

i

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

lub

f(g(), h(), j());
Johan Lundberg
źródło
Powiązane z Kolejnością oceny instrukcji przypisania w C ++ i Czy ten kod z „Języka programowania C ++” 4. wydanie, sekcja 36.3.6, ma dobrze zdefiniowane zachowanie? które są objęte papierem. Pierwsza z nich może stanowić fajne dodatkowe przykłady w twojej odpowiedzi poniżej.
Shafik Yaghmour

Odpowiedzi:

84

Niektóre typowe przypadki, w których kolejność oceny do tej pory nie została określona , są określone i ważne z C++17. Niektóre niezdefiniowane zachowania są teraz nieokreślone.

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

był nieokreślony, ale teraz jest nieokreślony. W szczególności nie określono kolejności, w jakiej każdy argument fjest oceniany względem innych. i++można ocenić wcześniej ilub odwrotnie. Rzeczywiście, może ocenić drugie wywołanie w innej kolejności, mimo że znajduje się w tym samym kompilatorze.

Jednak ocena każdego argumentu musi zostać wykonana w całości, ze wszystkimi skutkami ubocznymi, przed wykonaniem jakiegokolwiek innego argumentu. Więc możesz otrzymać f(1, 1)(drugi argument jest oceniany jako pierwszy) lub f(1, 2)(pierwszy argument jest oceniany jako pierwszy). Ale nigdy nie dostaniesz f(2, 2)ani niczego podobnego.

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

nie została określona, ​​ale stanie się zgodna z priorytetem operatorów, tak że pierwsza ocena fbędzie miała miejsce jako pierwsza w strumieniu (przykłady poniżej).

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

nadal ma nieokreśloną kolejność ocen g, h i j. Zauważ, że dla getf()(g(),h(),j()), reguły określają, że getf()będą oceniane wcześniej g, h, j.

Zwróć również uwagę na następujący przykład z tekstu oferty:

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

Przykład pochodzi z The C ++ Programming Language , 4th edition, Stroustrup i było wcześniej nieokreślonym zachowaniem, ale z C ++ 17 będzie działać zgodnie z oczekiwaniami. Podobne problemy występowały w przypadku funkcji wznawiania ( .then( . . . )).

Jako inny przykład rozważ następujące kwestie:

#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() ;
}

Z C ++ 14 i zanim będziemy mogli (i będziemy) uzyskiwać wyniki takie jak

play
no,and,Work,All,

zamiast

All,work,and,no,play

Zauważ, że powyższe działa tak samo jak

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

Ale nadal przed C ++ 17 nie było gwarancji, że pierwsze wywołania pojawią się jako pierwsze w strumieniu.

Referencje: Z zaakceptowanej propozycji :

Wyrażenia postfiksowe są oceniane od lewej do prawej. Obejmuje to wywołania funkcji i wyrażenia wyboru elementu członkowskiego.

Wyrażenia przypisania są oceniane od prawej do lewej. Obejmuje to zadania złożone.

Operandy do operatorów przesunięcia są obliczane od lewej do prawej. Podsumowując, poniższe wyrażenia są oceniane w kolejności a, następnie b, potem c, a następnie 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

Ponadto proponujemy następującą dodatkową regułę: kolejność oceny wyrażenia zawierającego przeciążony operator jest określona przez kolejność skojarzoną z odpowiednim operatorem wbudowanym, a nie przez reguły wywołań funkcji.

Uwaga dotycząca edycji: moja pierwotna odpowiedź została błędnie zinterpretowana a(b1, b2, b3). Kolejność b1, b2, b3nadal jest nieokreślony. (dziękuję @KABoissonneault, wszystkim komentującym.)

Jednak (jak @Yakk zaznacza) i to jest ważne: Nawet jeśli b1, b2, b3są wyrażeniami nietrywialne, każda z nich są całkowicie ocenione i przywiązany do odpowiedniego parametru funkcji zanim inni są te zaczęły być oceniane. Norma mówi tak:

§5.2.2 - Wywołanie funkcji 5.2.2.4:

. . . Wyrażenie postfiksowe jest sekwencjonowane przed każdym wyrażeniem na liście-wyrażeń i dowolnym argumencie domyślnym. Każde obliczenie wartości i efekt uboczny związany z inicjalizacją parametru oraz sama inicjalizacja są sekwencjonowane przed każdym obliczeniem wartości i efektem ubocznym związanym z inicjalizacją dowolnego kolejnego parametru.

Jednak jednego z tych nowych zdań brakuje w wersji roboczej GitHub :

Każde obliczenie wartości i efekt uboczny związany z inicjalizacją parametru oraz sama inicjalizacja są sekwencjonowane przed każdym obliczeniem wartości i efektem ubocznym związanym z inicjalizacją dowolnego kolejnego parametru.

Przykładem jest tam. Rozwiązuje problemy sprzed dziesięcioleci ( jak wyjaśnił Herb Sutter ) z wyjątkiem bezpieczeństwa

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

f(get_raw_a(), get_raw_a());

przeciekałby, gdyby jedno z wywołań get_raw_a()zgłosiło się, zanim drugi surowy wskaźnik zostałby powiązany z jego parametrem inteligentnego wskaźnika.

Jak wskazał TC, przykład jest błędny, ponieważ konstrukcja unique_ptr z surowego wskaźnika jest jawna, co zapobiega kompilacji. *

Zwróć również uwagę na to klasyczne pytanie (oznaczone jako C , a nie C ++ ):

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

jest nadal nieokreślona.

Johan Lundberg
źródło
1
„Druga, pomocnicza propozycja zastępuje kolejność oceny wywołań funkcji w następujący sposób: funkcja jest oceniana przed wszystkimi jej argumentami, ale każda para argumentów (z listy argumentów) jest nieokreślona kolejnością; co oznacza, że ​​jeden jest oceniany przed drugim, ale kolejność nie jest określona; gwarantuje się, że funkcja jest oceniana przed argumentami. Odzwierciedla to sugestię niektórych członków Core Working Group. "
Yakk - Adam Nevraumont
1
Odnoszę takie wrażenie z artykułu mówiącego, że „następujące wyrażenia są oceniane w kolejności a, potem b, cpotem d”, a następnie pokazuję a(b1, b2, b3), sugerując, że wszystkie bwyrażenia niekoniecznie są oceniane w dowolnej kolejności (w przeciwnym razie byłoby a(b, c, d))
KABoissonneault
1
@KABoissoneault, masz rację i odpowiednio zaktualizowałem odpowiedź. Ponadto wszystkie: cytaty pochodzą z wersji 3, która jest wersją głosowaną, o ile rozumiem.
Johan Lundberg,
2
@JohanLundberg Jest jeszcze jedna rzecz z gazety, która moim zdaniem jest ważna. a(b1()(), b2()())Można zamówić b1()()i b2()()w dowolnej kolejności, ale nie może zrobić b1()wtedy b2()()wtedy b1()(): to już nie może przeplot ich egzekucji. Krótko mówiąc, „8. ALTERNATYWNA KOLEJNOŚĆ OCENY POŁĄCZEŃ FUNKCYJNYCH” była częścią zatwierdzonej zmiany.
Yakk - Adam Nevraumont
3
f(i++, i)był niezdefiniowany. Jest teraz nieokreślony. Przykład łańcucha Stroustrupa był prawdopodobnie nieokreślony, a nie nieokreślony. `f (get_raw_a (), get_raw_a ());` nie skompiluje się, ponieważ odpowiedni unique_ptrkonstruktor jest jawny. Wreszcie x++ + ++xjest niezdefiniowana, kropka.
TC
46

Przeplot jest zabroniony w C ++ 17

W C ++ 14 następujące elementy były niebezpieczne:

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

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

Podczas wywołania funkcji mają miejsce cztery operacje

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

Ich kolejność była całkowicie nieokreślona, ​​a więc doskonale poprawna kolejność to (1), (3), (2), (4). Gdyby ta kolejność została wybrana i (3) zrzuca, to pamięć z (1) wycieka - jeszcze nie uruchomiliśmy (2), co by zapobiec wyciekowi.


W C ++ 17 nowe reguły zabraniają przeplotu. Z [intro.execution]:

Dla każdego wywołania funkcji F, dla każdej oceny A, która występuje w F i każdej oceny B, która nie występuje w F, ale jest oceniana w tym samym wątku i jako część tej samej obsługi sygnału (jeśli istnieje), jedno A jest sekwencjonowane przed B lub B jest sekwencjonowane przed A.

Jest przypis do tego zdania, który brzmi:

Innymi słowy, wykonania funkcji nie przeplatają się ze sobą.

To daje nam dwa prawidłowe uporządkowania: (1), (2), (3), (4) lub (3), (4), (1), (2). Nie jest określone, która kolejność jest pobierana, ale obie opcje są bezpieczne. Wszystkie porządki, w których (1) (3) występują zarówno przed (2), jak i (4), są teraz zabronione.

Barry
źródło
1
Trochę na bok, ale to był jeden z powodów dla boost :: make_shared, a później std :: make_shared (inny powód to mniej alokacji + lepsza lokalizacja). Wygląda na to, że motywacja dotycząca bezpieczeństwa wyjątków / wycieku zasobów już nie obowiązuje. Zobacz przykład kodu 3, boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/... Edit and stackoverflow.com/a/48844115 , Herbsutter.com/2013/05/29/gotw-89-solution- sprytni-pointers
Max Barraclough
3
Zastanawiam się, jak ta zmiana wpływa na optymalizację. Kompilator ma teraz znacznie zmniejszoną liczbę opcji, jak łączyć i przeplatać instrukcje procesora związane z obliczaniem argumentów, więc może to prowadzić do gorszego wykorzystania procesora?
Violet Giraffe
2

Znalazłem kilka uwag na temat kolejności oceny wyrażeń:

  • Szybka odpowiedź: Dlaczego język c ++ nie ma określonej kolejności obliczania argumentów funkcji?

    Pewna kolejność oceny gwarantuje otoczenie przeciążonych operatorów i reguł pełnych argumentów, które zostały dodane w C ++ 17. Ale pozostaje to, który argument jest pierwszy, pozostaje nieokreślony. W C ++ 17 jest teraz określone, że wyrażenie podające to, co należy wywołać (kod po lewej stronie (wywołania funkcji)) znajduje się przed argumentami, a którykolwiek argument jest oceniany jako pierwszy, jest w pełni oceniany przed następnym uruchomiona, aw przypadku metody obiektu wartość obiektu jest oceniana przed argumentami metody.

  • Kolejność wyceny

    21) Każde wyrażenie na liście wyrażeń oddzielonych przecinkami w inicjatorze w nawiasach jest oceniane tak, jak w przypadku wywołania funkcji ( nieokreślonej kolejności )

  • Niejednoznaczne wyrażenia

    Język C ++ nie gwarantuje kolejności, w jakiej argumenty wywołania funkcji są oceniane.

W P0145R3.Refining Expression Evaluation Order for Idiomatic C ++ znalazłem:

Obliczanie wartości i związany z nim efekt uboczny wyrażenia postfiksowego są sekwencjonowane przed wyrażeniami z listy wyrażeń. Inicjalizacje zadeklarowanych parametrów są sekwencjonowane w nieokreślony sposób bez przeplotu.

Ale nie znalazłem tego w standardzie, zamiast tego w standardzie znalazłem:

6.8.1.8 Wykonywanie sekwencyjne [intro.execution] że wyrażenie X jest sekwencjonowane przed wyrażeniem Y, jeśli każde obliczenie wartości i każdy efekt uboczny związany z wyrażeniem X jest sekwencjonowany przed każdym obliczeniem wartości i każdy efekt uboczny związany z wyrażeniem Y .

6.8.1.9 Wykonywanie sekwencyjne [intro.execution] Każde obliczenie wartości i efekt uboczny związane z pełnym wyrażeniem jest sekwencjonowane przed każdym obliczeniem wartości i efektem ubocznym związanym z następnym ocenianym pełnym wyrażeniem.

7.6.19.1 Operator przecinka [wyr. Comma] Para wyrażeń oddzielonych przecinkiem jest oceniana od lewej do prawej; ...

Więc porównałem zachowanie trzech kompilatorów dla standardów 14 i 17. Eksplorowany kod to:

#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());
}

Wyniki (tym bardziej spójne jest 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
źródło