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());
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());
Odpowiedzi:
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 f
jest oceniany względem innych. i++
można ocenić wcześniej i
lub 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 f
bę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:
- ab
- a-> b
- a -> * b
- a (b1, b2, b3)
- b @ = a
- a [b]
- a << b
- 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
, b3
nadal jest nieokreślony. (dziękuję @KABoissonneault, wszystkim komentującym.)
Jednak (jak @Yakk zaznacza) i to jest ważne: Nawet jeśli b1
, b2
, b3
są 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.
a
, potem b
, c
potem d
”, a następnie pokazuję a(b1, b2, b3)
, sugerując, że wszystkie b
wyrażenia niekoniecznie są oceniane w dowolnej kolejności (w przeciwnym razie byłoby a(b, c, d)
)
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.
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_ptr
konstruktor jest jawny. Wreszcie x++ + ++x
jest niezdefiniowana, kropka.
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
new A
unique_ptr<A>
konstruktornew B
unique_ptr<B>
konstruktorIch 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.
Znalazłem kilka uwag na temat kolejności oceny wyrażeń:
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.
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 )
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>