Questo codice della sezione 36.3.6 della quarta edizione di “The C ++ Programming Language” ha un comportamento ben definito?

In Bjarne Stroustrup’s La quarta edizione del linguaggio di programmazione C ++ sezione 36.3.6 Operazioni simili a STL il seguente codice viene utilizzato come esempio di concatenamento :

 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" ) ; } 

L’ assert non funziona in gcc ( vedi live ) e Visual Studio ( guardalo dal vivo ), ma non fallisce quando si usa Clang ( vederlo dal vivo ).

Perché sto ottenendo risultati diversi? Qualcuno di questi compilatori valuta erroneamente l’espressione concatenata o questo codice mostra una qualche forma di comportamento non specificato o non definito ?

Il codice mostra un comportamento non specificato a causa di un ordine non specificato di valutazione delle sottoespressioni anche se non richiama un comportamento indefinito poiché tutti gli effetti collaterali sono eseguiti all’interno di funzioni che introducono una relazione di sequenziamento tra gli effetti collaterali in questo caso.

Questo esempio è menzionato nella proposta N4228: Refining Expression Evaluation Order per Idiomatic C ++ che dice quanto segue riguardo al codice nella domanda:

[…] Questo codice è stato esaminato da esperti C ++ in tutto il mondo e pubblicato (The C ++ Programming Language, 4 th edition.) 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 concatenate di funzioni. Non è stato ovvio per me quando ho analizzato per la prima volta questo caso e apparentemente non a tutti i revisori esperti .

A prima vista può sembrare che, dal momento che ogni replace deve essere valutata da sinistra a destra, anche i corrispondenti gruppi di argomenti delle funzioni devono essere valutati come gruppi da sinistra a destra.

Questo non è corretto, gli argomenti di funzione hanno un ordine di valutazione non specificato, sebbene le chiamate di funzioni concatenate introducano un ordine di valutazione da sinistra a destra per ogni chiamata di funzione, gli argomenti di ogni chiamata di funzione sono solo sequenziati prima rispetto alla chiamata di funzione membro che fanno parte di. In particolare questo influisce sui seguenti inviti:

 s.find( "even" ) 

e:

 s.find( " don't" ) 

che sono sequenzialmente indeterminati rispetto a:

 s.replace(0, 4, "" ) 

le due chiamate di find potrebbero essere valutate prima o dopo la replace , il che importa dal momento che ha un effetto collaterale su s in un modo che altererebbe il risultato della find , cambia la lunghezza di s . Quindi, a seconda di quando viene valutata la replace rispetto alle due chiamate di find , il risultato sarà diverso.

Se guardiamo all’espressione concatenante ed esaminiamo l’ordine di valutazione di alcune delle sotto-espressioni:

 s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" ) ^ ^ ^ ^ ^ ^ ^ ^ ^ AB | | | 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 possono essere ulteriormente suddivisi in più sotto-espressioni. Così:

  • A è sequenziato prima di B che è sequenziato prima di C che è sequenziato prima di D
  • 1 a 9 sono sequenzialmente indeterminati rispetto ad altre sottoespressioni con alcune delle eccezioni elencate di seguito
    • 1 a 3 sono sequenziati prima di B
    • 4 a 6 sono sequenziati prima di C
    • 7 a 9 sono sequenziati prima di D

La chiave di questo problema è che:

  • 4 a 9 sono sequenzialmente indeterminati rispetto a B

Il potenziale ordine di scelta di valutazione per 4 e 7 rispetto a B spiega la differenza nei risultati tra clang e gcc nella valutazione di 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 ciascun caso:

 #include  #include  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 ( vederlo 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 ( vederlo 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 ( vederlo 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 se non vengono specificate le valutazioni delle sottoespressioni, a meno che non sia specificato, si tratta della bozza di standard C ++ 11 1.9 Esecuzione del programma che dice:

Tranne dove indicato, le valutazioni degli operandi dei singoli operatori e delle sottoespressioni delle singole espressioni non sono state prese in considerazione. [...]

e sappiamo che una chiamata di funzione introduce una relazione sequenziata prima della funzione chiama espressione e argomenti postfix rispetto al corpo della funzione, dalla sezione 1.9 :

[...] Quando si chiama una funzione (indipendentemente dal fatto che la funzione sia in linea), ogni calcolo del valore ed effetto collaterale associato a qualsiasi espressione di argomento, o con l'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 class e quindi il concatenamento valuteranno da sinistra a destra, dalla sezione 5.2.5 Accesso ai membri della class che dice:

[...] L'espressione postfissa prima del punto o della freccia viene valutata; 64 il risultato di tale valutazione, insieme con l'espressione id, determina il risultato dell'intera espressione postfissa.

Nota, nel caso in cui l' espressione-id finisca per essere una funzione membro non statica, non specifica l'ordine di valutazione della lista-espressioni all'interno di () poiché questa è una sotto-espressione separata. La grammatica pertinente da 5.2 Espressioni postfisso :

 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: perfezionare l'ordine di valutazione dell'espressione per Idiomatic C ++ ha apportato le varie modifiche. Comprese le modifiche che danno al codice un comportamento ben specificato rafforzando l'ordine delle regole di valutazione per le espressioni postfix e il loro elenco di espressioni .

[expr.call] p5 dice:

L'espressione postfix viene sequenziata prima di ogni espressione nell'elenco di espressioni e di qualsiasi argomento predefinito . L'inizializzazione di un parametro, incluso ogni calcolo del valore associato ed effetto collaterale, è sequenzialmente indeterminata rispetto a qualsiasi altro parametro. [Nota: tutti gli effetti collaterali delle valutazioni degli argomenti sono sequenziati prima dell'inserimento della 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]

Questo ha lo scopo di aggiungere informazioni sulla questione per quanto riguarda C ++ 17. La proposta ( Refining Expression Evaluation Order per Idiomatic C ++ Revision 2 ) per C++17 affrontato il problema citando il codice sopra riportato come esemplare.

Come suggerito, ho aggiunto informazioni rilevanti dalla proposta e per citare (evidenzia le mie):

L’ordine di valutazione dell’espressione, come è attualmente specificato nello standard, mina i consigli, gli idiomi di programmazione popolare o la relativa sicurezza delle strutture di libreria standard. Le trappole non sono solo per i principianti o per i programmatori incuranti. Colpiscono tutti noi indiscriminatamente, anche quando conosciamo le regole.

Considera il seguente frammento di programma:

 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"); } 

L’asserzione dovrebbe convalidare il risultato previsto del programmatore. Utilizza “concatenazione” di chiamate di funzioni membro, una pratica standard comune. Questo codice è stato esaminato da esperti C ++ in tutto il mondo e pubblicato (The C ++ Programming Language, 4th edition.) Tuttavia, la sua vulnerabilità a un ordine di valutazione non specificato è stata scoperta solo di recente da uno strumento.

Il documento suggeriva di modificare la regola precedente al C++17 sull’ordine di valutazione dell’espressione che era influenzata da C e esisteva da oltre tre decenni. Proponeva che il linguaggio dovrebbe garantire gli idiomi contemporanei o rischiare “trappole e fonti di oscuri e difficili da trovare bug” come quello che è successo con il codice esemplare sopra.

La proposta per C++17 richiede che ogni espressione abbia un ordine di valutazione ben definito :

  • Le espressioni postfix sono valutate da sinistra a destra. Ciò include le chiamate di funzioni e le espressioni di selezione dei membri.
  • Le espressioni di assegnazione sono valutate da destra a sinistra. Questo include i compiti composti.
  • Gli operatori per spostare gli operatori sono valutati da sinistra a destra.
  • L’ordine di valutazione di un’espressione che coinvolge un operatore sovraccarico è determinato dall’ordine associato all’operatore integrato corrispondente, non dalle regole per le chiamate di funzione.

Il codice sopra riportato viene compilato correttamente utilizzando GCC 7.1.1 e Clang 4.0.0 .