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:
[...] Ten kod został sprawdzony przez C ++ ekspertów na całym świecie, i opublikowane (C ++ Programming Language, 4 th Edition.) Jednak, jego podatność na nieokreślonym celu oceny została odkryta dopiero niedawno przez narzędzie [.. .]
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ń:
s.find( "even" )
i:
s.find( " don't" )
które są w nieokreślonej kolejności w odniesieniu do:
s.replace(0, 4, "" )
te dwa find
wezwania mogą być ocenione przed lub po replace
, co ma znaczenie, ponieważ ma wpływ uboczny s
w sposób, który zmieniłby wynik find
, zmienia długość s
. A więc w zależności od tego, kiedy replace
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:
s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
^ ^ ^ ^ ^ ^ ^ ^ ^
A B | | | C | | |
1 2 3 4 5 6
i:
.replace( s.find( " don't" ), 6, "" );
^ ^ ^ ^
D | | |
7 8 9
Zauważ, że ignorujemy fakt, że 4
i 7
można je dalej podzielić na więcej wyrażeń podrzędnych. Więc:
A
jest sekwencjonowany, przed B
który jest sekwencjonowany, przed C
który jest sekwencjonowany wcześniejD
1
to 9
są nieokreślone w kolejności w odniesieniu do innych wyrażeń podrzędnych z niektórymi wyjątkami wymienionymi poniżej
1
do 3
są sekwencjonowane wcześniejB
4
do 6
są sekwencjonowane wcześniejC
7
do 9
są sekwencjonowane wcześniejD
Kluczem do tego problemu jest to, że:
4
aby 9
zostały indeterminately zsekwencjonowany w odniesieniu doB
Potencjalna kolejność wyboru ewaluacji dla 4
i 7
w odniesieniu do B
wyjaśnia różnicę w wynikach pomiędzy oceną clang
i gcc
podczas oceny f2()
. W moich testach clang
ocenia B
przed oceną 4
i 7
podczas gcc
ocenia ją po. Możemy skorzystać z następującego programu testowego, aby zademonstrować, co się dzieje w każdym przypadku:
#include <iostream>
#include <string>
std::string::size_type my_find( std::string s, const char *cs )
{
std::string::size_type pos = s.find( cs ) ;
std::cout << "position " << cs << " found in complete expression: "
<< pos << std::endl ;
return pos ;
}
int main()
{
std::string s = "but I have heard it works even if you don't believe in it" ;
std::string copy_s = s ;
std::cout << "position of even before s.replace(0, 4, \"\" ): "
<< s.find( "even" ) << std::endl ;
std::cout << "position of don't before s.replace(0, 4, \"\" ): "
<< s.find( " don't" ) << std::endl << std::endl;
copy_s.replace(0, 4, "" ) ;
std::cout << "position of even after s.replace(0, 4, \"\" ): "
<< copy_s.find( "even" ) << std::endl ;
std::cout << "position of don't after s.replace(0, 4, \"\" ): "
<< copy_s.find( " don't" ) << std::endl << std::endl;
s.replace(0, 4, "" ).replace( my_find( s, "even" ) , 4, "only" )
.replace( my_find( s, " don't" ), 6, "" );
std::cout << "Result: " << s << std::endl ;
}
Wynik dla gcc
( zobacz na żywo )
position of even before s.replace(0, 4, "" ): 26
position of don't before s.replace(0, 4, "" ): 37
position of even after s.replace(0, 4, "" ): 22
position of don't after s.replace(0, 4, "" ): 33
position don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it
Wynik dla clang
( zobacz na żywo ):
position of even before s.replace(0, 4, "" ): 26
position of don't before s.replace(0, 4, "" ): 37
position of even after s.replace(0, 4, "" ): 22
position of don't after s.replace(0, 4, "" ): 33
position even found in complete expression: 22
position don't found in complete expression: 33
Result: I have heard it works only if you believe in it
Wynik dla Visual Studio
( zobacz na żywo ):
position of even before s.replace(0, 4, "" ): 26
position of don't before s.replace(0, 4, "" ): 37
position of even after s.replace(0, 4, "" ): 22
position of don't after s.replace(0, 4, "" ): 33
position don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it
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:
O ile nie zaznaczono inaczej, oceny operandów poszczególnych operatorów i podwyrażeń poszczególnych wyrażeń nie są sekwencjonowane. [...]
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
:
[...] Podczas wywoływania funkcji (niezależnie od tego, czy funkcja jest wbudowana, czy nie), każde obliczenie wartości i efekt uboczny związane z jakimkolwiek wyrażeniem argumentu lub z wyrażeniem z przyrostkiem oznaczającym wywoływaną funkcję jest sekwencjonowane przed wykonaniem każdego wyrażenia lub instrukcji w treści wywoływanej funkcji. [...]
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:
[...] Wyrażenie z przyrostkiem przed kropką lub strzałką jest obliczane; 64
wynik tej oceny, wraz z wyrażeniem id, określa wynik całego wyrażenia postfiksowego.
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 z 5.2
wyrażeń Postfix :
postfix-expression:
postfix-expression ( expression-listopt) // function call
postfix-expression . templateopt id-expression // Class member access, ends
// up as a postfix-expression
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:
Wyrażenie postfiksowe jest sekwencjonowane przed każdym wyrażeniem na liście-wyrażeń i dowolnym argumencie domyślnym . Inicjalizacja parametru, w tym każde skojarzone obliczenie wartości i efekt uboczny, jest nieokreślone w kolejności w odniesieniu do dowolnego innego parametru. [Uwaga: Wszystkie efekty uboczne oceny argumentów są sekwencjonowane przed wejściem do funkcji (patrz 4.6). —End note] [Przykład:
void f() {
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"); // OK
}
- koniec przykładu]
s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );