W sekcji C ++ Programming Language Bjarne Stroustrupa, 4. wydanie, sekcja 36.3.6
Operacje podobne do STL, następujący kod jest używany jako przykład łączenia :
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" ) ;
}
Assert zawodzi gcc
( zobacz na żywo ) i Visual Studio
( zobacz na żywo ), ale nie zawodzi, gdy użyjesz Clang ( zobacz na żywo ).
Dlaczego otrzymuję różne wyniki? Czy któryś z tych kompilatorów nieprawidłowo ocenia wyrażenie łańcuchowe, czy też ten kod wykazuje jakąś formę nieokreślonego lub niezdefiniowanego zachowania ?
c++
c++11
language-lawyer
operator-precedence
unspecified-behavior
Shafik Yaghmour
źródło
źródło
s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
cout << a << b << c
≡operator<<(operator<<(operator<<(cout, a), b), c)
jest tylko nieznacznie mniej brzydki.Odpowiedzi:
Kod wykazuje nieokreślone zachowanie ze względu na nieokreśloną kolejność oceny wyrażeń podrzędnych, chociaż nie wywołuje niezdefiniowanego zachowania, ponieważ wszystkie efekty uboczne są wykonywane w ramach funkcji, które wprowadzają zależność sekwencjonowania w tym przypadku między efektami ubocznymi.
Ten przykład jest wymieniony w propozycji N4228: Refining Expression Evaluation Order for Idiomatic C ++, która mówi co następuje o kodzie w pytaniu:
Detale
Dla wielu osób może być oczywiste, że argumenty funkcji mają nieokreśloną kolejność oceny, ale prawdopodobnie nie jest tak oczywiste, jak to zachowanie współdziała z wywołaniami funkcji połączonych łańcuchami. Nie było to dla mnie oczywiste, kiedy po raz pierwszy analizowałem ten przypadek i najwyraźniej nie dla wszystkich ekspertów-recenzentów .
Na pierwszy rzut oka może się wydawać, że od każdego
replace
musi być oceniana od lewej do prawej, to odpowiednie grupy argumentów funkcji muszą być również oceniane jako grupy od lewej do prawej.Jest to niepoprawne, argumenty funkcji mają nieokreśloną kolejność obliczania, chociaż wywołania funkcji w łańcuchu wprowadzają kolejność obliczania od lewej do prawej dla każdego wywołania funkcji, argumenty każdego wywołania funkcji są sekwencjonowane tylko przed wywołaniem funkcji składowej, których są częścią z. W szczególności dotyczy to następujących wezwań:
i:
które są w nieokreślonej kolejności w odniesieniu do:
te dwa
find
wezwania mogą być ocenione przed lub poreplace
, co ma znaczenie, ponieważ ma wpływ ubocznys
w sposób, który zmieniłby wynikfind
, zmienia długośćs
. A więc w zależności od tego, kiedyreplace
jest to oceniane względem tych dwóchfind
wywołań, wynik będzie się różnić.Jeśli spojrzymy na wyrażenie łańcuchowe i zbadamy kolejność oceny niektórych wyrażeń podrzędnych:
i:
Zauważ, że ignorujemy fakt, że
4
i7
można je dalej podzielić na więcej wyrażeń podrzędnych. Więc:A
jest sekwencjonowany, przedB
który jest sekwencjonowany, przedC
który jest sekwencjonowany wcześniejD
1
to9
są nieokreślone w kolejności w odniesieniu do innych wyrażeń podrzędnych z niektórymi wyjątkami wymienionymi poniżej1
do3
są sekwencjonowane wcześniejB
4
do6
są sekwencjonowane wcześniejC
7
do9
są sekwencjonowane wcześniejD
Kluczem do tego problemu jest to, że:
4
aby9
zostały indeterminately zsekwencjonowany w odniesieniu doB
Potencjalna kolejność wyboru ewaluacji dla
4
i7
w odniesieniu doB
wyjaśnia różnicę w wynikach pomiędzy ocenąclang
igcc
podczas ocenyf2()
. W moich testachclang
oceniaB
przed oceną4
i7
podczasgcc
ocenia ją po. Możemy skorzystać z następującego programu testowego, aby zademonstrować, co się dzieje w każdym przypadku:Wynik dla
gcc
( zobacz na żywo )Wynik dla
clang
( zobacz na żywo ):Wynik dla
Visual Studio
( zobacz na żywo ):Szczegóły ze standardu
Wiemy, że o ile nie określono inaczej, oceny wyrażeń podrzędnych są bez kolejności, pochodzi to z projektu standardowej sekcji C ++ 11
1.9
Wykonanie programu, która mówi:i wiemy, że wywołanie funkcji wprowadza sekwencję, zanim relacja funkcji wywoła wyrażenie postfix i argumenty w odniesieniu do ciała funkcji, z sekcji
1.9
:Wiemy również, że dostęp do członków klasy, a tym samym tworzenie łańcuchów, będą oceniane od lewej do prawej, z sekcji
5.2.5
Dostęp do członków klasy, która mówi:Należy zauważyć, że w przypadku, gdy wyrażenie-id staje się niestatyczną funkcją składową, nie określa kolejności oceny listy-wyrażeń w ramach,
()
ponieważ jest to oddzielne wyrażenie podrzędne. Odpowiednia gramatyka z5.2
wyrażeń Postfix :Zmiany w C ++ 17
Wniosek p0145r3: Refining Expression Evaluation Order for Idiomatic C ++ wprowadził kilka zmian. Obejmuje zmiany, które zapewniają kodowi dobrze określone zachowanie, wzmacniając kolejność reguł oceny dla wyrażeń postfiksowych i ich listy wyrażeń .
[wyr.call] p5 mówi:
źródło
"even"
,"don't"
oraz kilka przypadkóws
jest unsequenced względem siebie.foo().func( bar() )
. Może zadzwonićfoo()
przed lub po wywołaniubar()
. Wyrażenie postfiksowe tofoo().func
. Argumenty i wyrażenie postfiksowe są sekwencjonowane przedfunc()
treścią, ale bez kolejności względem siebie.Ma to na celu dodanie informacji na ten temat w odniesieniu do C ++ 17. Propozycja ( Refining Expression Evaluation Order for Idiomatic C ++ Revision 2 ) dotycząca
C++17
rozwiązania problemu z przytoczeniem powyższego kodu była wzorem.Zgodnie z sugestią, dodałem odpowiednie informacje z propozycji i zacytowałem (podkreśla moje):
W artykule zaproponowano zmianę
C++17
reguły wstępnej dotyczącej kolejności oceny ekspresji, na którą wpłynęłoC
i istnieje od ponad trzech dekad. Zaproponowano, że język powinien gwarantować współczesne idiomy lub ryzykować „pułapki i źródła niejasnych, trudnych do znalezienia błędów” takich jak to, co stało się z powyższym przykładem kodu.Propozycja
C++17
ma wymagać, aby każde wyrażenie miało dobrze zdefiniowaną kolejność oceny :Powyższy kod kompiluje się pomyślnie przy użyciu
GCC 7.1.1
iClang 4.0.0
.źródło