Ordine di esecuzione C ++ nel concatenamento dei metodi


108

L'output di questo programma:

#include <iostream> 
class c1
{   
  public:
    c1& meth1(int* ar) {
      std::cout << "method 1" << std::endl;
      *ar = 1;
      return *this;
    }
    void meth2(int ar)
    {
      std::cout << "method 2:"<< ar << std::endl;
    }
};

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu).meth2(nu);
}

È:

method 1
method 2:0

Perché nunon è 1 quando meth2()inizia?


41
@MartinBonner: Anche se conosco la risposta, non la definirei "ovvia" in nessun senso della parola e, anche se lo fosse, non sarebbe una buona ragione per votare in meno. ! deludente
Gare di leggerezza in orbita

4
Questo è ciò che ottieni quando modifichi i tuoi argomenti. Le funzioni che modificano i loro argomenti sono più difficili da leggere, i loro effetti sono inaspettati per il prossimo programmatore che lavora sul codice e portano a sorprese come questa. Consiglio vivamente di evitare di modificare qualsiasi parametro tranne l'invocante. Modificare l'invocante non sarebbe un problema qui, perché il secondo metodo viene chiamato sul risultato del primo, quindi gli effetti sono ordinati su di esso. Ci sono ancora alcuni casi in cui non sarebbero però.
Jan Hudec


@JanHudec Questo è precisamente il motivo per cui la programmazione funzionale pone così grande enfasi sulla purezza delle funzioni.
Pharap

2
Ad esempio, una convenzione di chiamata basata sullo stack probabilmente preferirebbe eseguire il push nu, &nue cnello stack in quell'ordine, quindi invocare meth1, inviare il risultato allo stack, quindi invocare meth2, mentre una convenzione di chiamata basata su registro vorrebbe caricare ce &nunei registri, invocare meth1, caricare nuin un registro, quindi invocare meth2.
Neil

Risposte:


66

Perché l'ordine di valutazione non è specificato.

Stai vedendo nuin mainessere valutato 0prima ancorameth1 essere chiamato. Questo è il problema del concatenamento. Consiglio di non farlo.

Crea solo un programma carino, semplice, chiaro, di facile lettura e di facile comprensione:

int main()
{
  c1 c;
  int nu = 0;
  c.meth1(&nu);
  c.meth2(nu);
}

14
C'è la possibilità che una proposta per chiarire l'ordine di valutazione in alcuni casi , che risolve questo problema, arrivi per C ++ 17
Revolver_Ocelot

7
Mi piace il concatenamento di metodi (ad esempio <<per l'output e "costruttori di oggetti" per oggetti complessi con troppi argomenti per i costruttori - ma si mescola davvero male con gli argomenti di output.
Martin Bonner supporta Monica

34
Lo capisco bene? l'ordine di valutazione di meth1ed meth2è definito, ma la valutazione del parametro meth2può avvenire prima che meth1venga chiamato ...?
Roddy

7
Il concatenamento di metodi va bene fintanto che i metodi sono ragionevoli e modificano solo l'invocante (per il quale gli effetti sono ben ordinati, perché il secondo metodo viene chiamato sul risultato del primo).
Jan Hudec

4
È logico, se ci pensi. Funziona comemeth2(meth1(c, &nu), nu)
BartekChom

29

Penso che questa parte della bozza di standard riguardante l'ordine di valutazione sia rilevante:

1.9 Esecuzione del programma

...

  1. Tranne dove indicato, le valutazioni degli operandi dei singoli operatori e delle sottoespressioni delle singole espressioni non sono sequenziali. I calcoli del valore degli operandi di un operatore vengono sequenziati prima del calcolo del valore del risultato dell'operatore. Se un effetto collaterale su un oggetto scalare non viene sequenziato rispetto a un altro effetto collaterale sullo stesso oggetto scalare o un calcolo del valore utilizzando il valore dello stesso oggetto scalare e non sono potenzialmente concorrenti, il comportamento non è definito

e anche:

5.2.2 Chiamata di funzione

...

  1. [Nota: le valutazioni dell'espressione postfissa e degli argomenti sono tutte senza sequenze l'una rispetto all'altra. Tutti gli effetti collaterali delle valutazioni degli argomenti vengono sequenziati prima di inserire la funzione - nota finale]

Quindi per la tua linea c.meth1(&nu).meth2(nu);, considera cosa sta succedendo in operator in termini di operatore di chiamata di funzione per la chiamata finale a meth2, in modo da vedere chiaramente la scomposizione nell'espressione e nell'argomento postfisso nu:

operator()(c.meth1(&nu).meth2, nu);

Le valutazioni dell'espressione postfissa e dell'argomento per la chiamata di funzione finale (cioè l'espressione postfissa c.meth1(&nu).meth2e nu) non sono sequenziali l'una rispetto all'altra secondo la regola della chiamata di funzione sopra. Pertanto, l' effetto collaterale del calcolo dell'espressione postfissa sull'oggetto scalare non arè sottoposto a sequenze rispetto alla valutazione dell'argomento nuprima della meth2chiamata alla funzione. In base alla regola di esecuzione del programma sopra, questo è un comportamento non definito.

In altre parole, non è necessario che il compilatore valuti l' nuargomento della meth2chiamata dopo la meth1chiamata: è libero di assumere alcun effetto collaterale di meth1influenzare ilnu valutazione.

Il codice assembly prodotto da quanto sopra contiene la seguente sequenza nella mainfunzione:

  1. Variabile nu viene allocata nello stack e inizializzata con 0.
  2. Un registro (ebx nel mio caso) riceve una copia del valore dinu
  3. Gli indirizzi di nuec vengono caricati nei registri dei parametri
  4. meth1 è chiamato
  5. Il registro del valore restituito e il valore precedentemente memorizzato nella cache di nuinebx registro vengono caricati nei registri di parametro
  6. meth2 è chiamato

Fondamentalmente, nel passaggio 5 sopra il compilatore consente nudi riutilizzare il valore memorizzato nella cache del passaggio 2 nella chiamata di funzione a meth2. Qui si ignora la possibilità che nupotrebbe essere stata modificata dalla chiamata ameth1 "comportamento indefinito" in azione.

NOTA: questa risposta è cambiata nella sostanza rispetto alla sua forma originale. La mia spiegazione iniziale in termini di effetti collaterali del calcolo dell'operando non sequenziato prima della chiamata finale della funzione non era corretta, perché lo sono. Il problema è il fatto che il calcolo degli operandi stessi è in sequenza indeterminata.


2
Questo è sbagliato. Le chiamate di funzione sono sequenziate in modo indeterminato w / r / t altre valutazioni nella funzione chiamante (a meno che non sia altrimenti imposto un vincolo sequencing-before); non si alternano.
TC

1
@TC - Non ho mai detto nulla sulle chiamate di funzione che vengono intercalate. Ho solo fatto riferimento agli effetti collaterali degli operatori. Se guardi il codice assembly prodotto da quanto sopra, vedrai che meth1viene eseguito prima meth2, ma il parametro per meth2è un valore numemorizzato nella cache in un registro prima della chiamata a meth1- cioè il compilatore ha ignorato i potenziali effetti collaterali, che è coerente con la mia risposta.
Smeeheey

1
Stai esattamente affermando che - "non è garantito che il suo effetto collaterale (cioè l'impostazione del valore di ar) sia sequenziato prima della chiamata". La valutazione dell'espressione postfissa in una chiamata di funzione (che è c.meth1(&nu).meth2) e la valutazione dell'argomento di quella chiamata ( nu) sono generalmente non sequenziate, ma 1) i loro effetti collaterali sono tutti sequenziati prima dell'ingresso in meth2e 2) poiché c.meth1(&nu)è una chiamata di funzione , è sequenziato in modo indeterminato con la valutazione di nu. All'interno meth2, se in qualche modo ottenesse un puntatore alla variabile in main, vedrebbe sempre 1.
TC

2
"Tuttavia, non è garantito che l'effetto collaterale del calcolo degli operandi (cioè l'impostazione del valore di ar) sia sequenziato prima di qualsiasi cosa (come per 2) sopra)." È assolutamente garantito che sia sequenziato prima della chiamata a meth2, come indicato al punto 3 della pagina cppreference che stai citando (che hai anche trascurato di citare correttamente).
TC

1
Hai preso qualcosa di sbagliato e lo hai peggiorato. Non c'è assolutamente alcun comportamento indefinito qui. Continua a leggere [intro.execution] / 15, oltre l'esempio.
TC

9

Nello standard C ++ del 1998, Sezione 5, paragrafo 4

Tranne dove indicato, l'ordine di valutazione degli operandi dei singoli operatori e delle sottoespressioni delle singole espressioni e l'ordine in cui si verificano gli effetti collaterali non è specificato. Tra il punto della sequenza precedente e quello successivo, un oggetto scalare deve avere il suo valore memorizzato modificato al massimo una volta dalla valutazione di un'espressione. Inoltre, si accede al valore precedente solo per determinare il valore da memorizzare. I requisiti del presente paragrafo devono essere soddisfatti per ogni ordinamento ammissibile delle sottoespressioni di un'espressione completa; altrimenti il ​​comportamento è indefinito.

(Ho omesso un riferimento alla nota # 53 che non è rilevante per questa domanda).

In sostanza, &nudeve essere valutato prima di chiamare c1::meth1()e nudeve essere valutato prima di chiamare c1::meth2(). Tuttavia, non vi è alcun requisito nuda valutare prima &nu(ad esempio, è consentito che nuvenga valutato prima, poi &nue poi c1::meth1()chiamato - che potrebbe essere ciò che sta facendo il compilatore). Non è quindi garantito che l' espressione *ar = 1in c1::meth1()venga valutata prima che nuin main()venga valutata, in modo da essere passata ac1::meth2() .

Gli standard C ++ successivi (che attualmente non ho sul PC che sto usando questa sera) hanno essenzialmente la stessa clausola.


7

Penso che durante la compilazione, prima che le funzioni meth1 e meth2 siano realmente chiamate, i parametri siano stati passati a loro. Voglio dire quando usi "c.meth1 (& nu) .meth2 (nu);" il valore nu = 0 è stato passato a meth2, quindi non importa se "nu" viene cambiato per ultimo.

puoi provare questo:

#include <iostream> 
class c1
{
public:
    c1& meth1(int* ar) {
        std::cout << "method 1" << std::endl;
        *ar = 1;
        return *this;
    }
    void meth2(int* ar)
    {
        std::cout << "method 2:" << *ar << std::endl;
    }
};

int main()
{
    c1 c;
    int nu = 0;
    c.meth1(&nu).meth2(&nu);
    getchar();
}

otterrà la risposta che desideri

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.