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 replacedeve 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 findchiamate potrebbero essere valutate prima o dopo replace, il che è importante poiché ha un effetto collaterale sin un modo che altererebbe il risultato di find, cambia la durata di s. Quindi, a seconda di quando replaceviene valutato rispetto alle due findchiamate, 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 4e 7può essere ulteriormente suddiviso in più sottoespressioni. Così:
Aè sequenziato prima di Bcui è sequenziato prima di Ccui è sequenziato primaD
1a 9sono indeterminatamente sequenziato rispetto ad altri sotto-espressioni con alcune delle eccezioni elencate di seguito
1a 3sono sequenziato primaB
4a 6sono sequenziato primaC
7a 9sono sequenziato primaD
La chiave di questo problema è che:
4per 9sono indeterminatamente sequenziato rispettoB
Il potenziale ordine di scelta della valutazione per 4e 7rispetto a Bspiega la differenza nei risultati tra clange gccdurante la valutazione f2(). Nei miei test clangvaluta Bprima di valutare 4e 7mentre gccvaluta 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, "" );