Il codice mostra un comportamento non specificato a causa di un ordine di valutazione non specificato delle sottoespressioni sebbene non invoca un comportamento indefinito poiché tutti gli effetti collaterali vengono eseguiti all'interno di funzioni che in questo caso introduce una relazione di sequenziamento tra gli effetti collaterali.
Questo esempio è menzionato nella proposta N4228: Refining Expression Evaluation Order for Idiomatic C ++ che dice quanto segue sul codice nella domanda:
[...] Questo codice è stato esaminato da esperti di C ++ in tutto il mondo e pubblicato (The C ++ Programming Language, 4a edizione.) Tuttavia, la sua vulnerabilità a un ordine di valutazione non specificato è stata scoperta solo di recente da uno strumento [.. .]
Dettagli
Può essere ovvio per molti che gli argomenti delle funzioni hanno un ordine di valutazione non specificato, ma probabilmente non è così ovvio come questo comportamento interagisca con le chiamate di funzioni concatenate. Non era ovvio per me quando ho analizzato per la prima volta questo caso e, a quanto pare, nemmeno per tutti i revisori esperti .
A prima vista può sembrare che, poiché ciascuno replace
deve essere valutato da sinistra a destra, anche i gruppi di argomenti della funzione corrispondenti devono essere valutati come gruppi da sinistra a destra.
Questo non è corretto, gli argomenti delle funzioni hanno un ordine di valutazione non specificato, sebbene il concatenamento delle chiamate di funzione introduca un ordine di valutazione da sinistra a destra per ogni chiamata di funzione, gli argomenti di ciascuna chiamata di funzione sono solo in sequenza prima rispetto alla chiamata di funzione membro di cui fanno parte di. In particolare ciò influisce sui seguenti bandi:
s.find( "even" )
e:
s.find( " don't" )
che sono in sequenza indeterminata rispetto a:
s.replace(0, 4, "" )
le due find
chiamate potrebbero essere valutate prima o dopo replace
, il che è importante poiché ha un effetto collaterale s
in un modo che altererebbe il risultato di find
, cambia la durata di s
. Quindi, a seconda di quando replace
viene valutato rispetto alle due find
chiamate, il risultato sarà diverso.
Se guardiamo l'espressione concatenata ed esaminiamo l'ordine di valutazione di alcune delle sottoespressioni:
s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
^ ^ ^ ^ ^ ^ ^ ^ ^
A B | | | C | | |
1 2 3 4 5 6
e:
.replace( s.find( " don't" ), 6, "" );
^ ^ ^ ^
D | | |
7 8 9
Nota, stiamo ignorando il fatto che 4
e 7
può essere ulteriormente suddiviso in più sottoespressioni. Così:
A
è sequenziato prima di B
cui è sequenziato prima di C
cui è sequenziato primaD
1
a 9
sono indeterminatamente sequenziato rispetto ad altri sotto-espressioni con alcune delle eccezioni elencate di seguito
1
a 3
sono sequenziato primaB
4
a 6
sono sequenziato primaC
7
a 9
sono sequenziato primaD
La chiave di questo problema è che:
4
per 9
sono indeterminatamente sequenziato rispettoB
Il potenziale ordine di scelta della valutazione per 4
e 7
rispetto a B
spiega la differenza nei risultati tra clang
e gcc
durante la valutazione f2()
. Nei miei test clang
valuta B
prima di valutare 4
e 7
mentre gcc
valuta dopo. Possiamo usare il seguente programma di test per dimostrare cosa sta succedendo in ogni caso:
#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 ;
}
Risultato per gcc
( guardalo dal vivo )
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
Risultato per clang
( guardalo dal vivo ):
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
Risultato per Visual Studio
( guardalo dal vivo ):
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
Dettagli dallo standard
Sappiamo che, a meno che non sia specificato, le valutazioni delle sottoespressioni non sono sequenziali, questo è dalla bozza di 1.9
esecuzione del programma della sezione standard C ++ 11 che dice:
Salvo ove diversamente specificato, le valutazioni degli operandi dei singoli operatori e delle sottoespressioni delle singole espressioni non vengono sequestrate. [...]
e sappiamo che una chiamata di funzione introduce una relazione prima in sequenza della funzione chiama espressione postfissa e argomenti rispetto al corpo della funzione, dalla sezione 1.9
:
[...] Quando si chiama una funzione (indipendentemente dal fatto che la funzione sia inline o meno), ogni calcolo di valore ed effetto collaterale associato a qualsiasi espressione di argomento, o all'espressione postfissa che designa la funzione chiamata, viene sequenziato prima dell'esecuzione di ogni espressione o istruzione nel corpo della funzione chiamata. [...]
Sappiamo anche che l'accesso ai membri della classe e quindi il concatenamento valuterà da sinistra a destra, dalla sezione 5.2.5
Accesso ai membri della classe che dice:
[...] Viene valutata l'espressione postfissa prima del punto o della freccia; 64
il risultato di quella valutazione, insieme all'espressione id, determina il risultato dell'intera espressione postfissa.
Nota, nel caso in cui l' espressione id finisce per essere una funzione membro non statica, non specifica l'ordine di valutazione dell'elenco delle espressioni all'interno di ()
poiché si tratta di una sottoespressione separata. La grammatica pertinente dalle 5.2
espressioni Postfix :
postfix-expression:
postfix-expression ( expression-listopt) // function call
postfix-expression . templateopt id-expression // Class member access, ends
// up as a postfix-expression
C ++ 17 modifiche
La proposta p0145r3: Affinamento dell'ordine di valutazione delle espressioni per Idiomatic C ++ ha apportato diverse modifiche. Comprese modifiche che danno al codice un comportamento ben specificato rafforzando l'ordine delle regole di valutazione per le espressioni postfisse e il loro elenco di espressioni .
[expr.call] p5 dice:
L'espressione postfissa è in sequenza prima di ogni espressione nell'elenco delle espressioni e di qualsiasi argomento predefinito . L'inizializzazione di un parametro, incluso ogni calcolo del valore associato e l'effetto collaterale, è in sequenza indeterminata rispetto a quella di qualsiasi altro parametro. [Nota: tutti gli effetti collaterali delle valutazioni degli argomenti vengono sequenziati prima di entrare nella funzione (vedere 4.6). —End note] [Esempio:
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
}
—End esempio]
s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );