Questo codice della sezione 36.3.6 della quarta edizione del "linguaggio di programmazione C ++" ha un comportamento ben definito?


94

Nella sezione Operazioni simili a STL di Bjarne Stroustrup The C ++ Programming Language 4th edition, il codice seguente viene utilizzato come esempio di concatenamento :36.3.6

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 fallisce gcc( guardalo dal vivo ) e Visual Studio( guardalo dal vivo ), ma non fallisce quando usi Clang ( guardalo dal vivo ).

Perché ottengo risultati diversi? Qualcuno di questi compilatori sta valutando in modo errato l'espressione concatenata o questo codice mostra una qualche forma di comportamento non specificato o non definito ?


Meglio:s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
Ben Voigt

20
bug a parte, sono l'unico che pensa che codice brutto come quello non dovrebbe essere nel libro?
Karoly Horvath,

5
@KarolyHorvath Nota che cout << a << b << coperator<<(operator<<(operator<<(cout, a), b), c)è solo marginalmente meno brutto.
Oktalist

1
@Oktalist: :) almeno ho capito l'intenzione lì. insegna la ricerca del nome dipendente dall'argomento e la sintassi dell'operatore allo stesso tempo in un formato conciso ... e non dà l'impressione che si debba effettivamente scrivere codice del genere.
Karoly Horvath

Risposte:


104

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]


7
Sono un po 'sorpreso di vedere che "molti esperti" hanno trascurato il problema, è noto che la valutazione dell'espressione postfissa di una chiamata di funzione non è sequenziata prima di valutare gli argomenti (in tutte le versioni di C e C ++).
MM

@ShafikYaghmour Le chiamate di funzione sono in sequenza indeterminata l'una rispetto all'altra e qualsiasi altra cosa, ad eccezione delle relazioni sequenziate prima che hai notato. Tuttavia, la valutazione di 1, 2, 3, 5, 6, 8, 9, "even", "don't"ei diversi casi di ssono non in sequenza rispetto all'altra.
TC

4
@TC no non lo è (ecco come si presenta questo "bug"). Ad esempio foo().func( bar() ), potrebbe chiamare foo()prima o dopo la chiamata bar(). L' espressione postfissa è foo().func. Gli argomenti e l'espressione postfissa sono sequenziati prima del corpo di func(), ma senza sequenze l'uno rispetto all'altro.
MM

@ MattMcNabb Ah, giusto, ho letto male. Stai parlando dell'espressione postfissa stessa piuttosto che della chiamata. Sì, è vero, non sono sequestrati (a meno che non si applichi qualche altra regola, ovviamente).
TC

6
C'è anche il fatto che si tende a presumere che il codice che appare in un libro di B.Stroustrup sia corretto altrimenti qualcuno l'avrebbe sicuramente notato! (correlato; gli utenti SO trovano ancora nuovi errori in K&R)
MM

4

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

Come suggerito, ho aggiunto informazioni pertinenti dalla proposta e da citare (evidenzia la mia):

L'ordine di valutazione delle espressioni, come è attualmente specificato nello standard, mina i consigli, i comuni linguaggi di programmazione o la relativa sicurezza delle strutture delle biblioteche standard. Le trappole non sono solo per i principianti o per i programmatori incuranti. Influenzano 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 dal programmatore. Utilizza il "concatenamento" delle chiamate di funzioni dei membri, una pratica standard comune. 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.

Il documento ha suggerito di cambiare la C++17regola sulla valutazione dell'ordine di espressione che è stata influenzata Ce che esiste da più di tre decenni. Ha proposto che il linguaggio dovrebbe garantire idiomi contemporanei o rischiare "trappole e fonti di bug oscuri e difficili da trovare" come quello che è successo con l'esempio di codice sopra.

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

  • Le espressioni con suffisso vengono valutate da sinistra a destra. Ciò include chiamate di funzioni ed espressioni di selezione dei membri.
  • Le espressioni di assegnazione vengono valutate da destra a sinistra. Questo include incarichi composti.
  • Gli operandi per spostare gli operatori vengono valutati da sinistra a destra.
  • L'ordine di valutazione di un'espressione che coinvolge un operatore sovraccarico è determinato dall'ordine associato al corrispondente operatore incorporato, non dalle regole per le chiamate di funzione.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.