Qual è la risposta corretta per cout << a ++ << a ;?


98

Recentemente in un'intervista c'era una seguente domanda di tipo oggettivo.

int a = 0;
cout << a++ << a;

Risposte:

un. 10
b. 01
c. comportamento indefinito

Ho risposto alla scelta b, ovvero l'output sarebbe "01".

Ma in seguito, con mia grande sorpresa, un intervistatore mi ha detto che la risposta corretta è l'opzione c: non definita.

Ora, conosco il concetto di punti di sequenza in C ++. Il comportamento non è definito per la seguente istruzione:

int i = 0;
i += i++ + i++;

ma secondo la mia comprensione per la dichiarazione cout << a++ << a, ostream.operator<<()sarebbe stato chiamato due volte, prima con ostream.operator<<(a++)e dopo ostream.operator<<(a).

Ho anche controllato il risultato sul compilatore VS2010 e anche il suo output è "01".


30
Hai chiesto una spiegazione? Spesso intervisto potenziali candidati e sono piuttosto interessato a ricevere domande, mostra interesse.
Brady

3
@jrok È un comportamento indefinito. Tutto ciò che fa l'implementazione (incluso l'invio di un'e-mail insultante a tuo nome al tuo capo) è conforme.
James Kanze

2
Questa domanda richiede una risposta C ++ 11 (la versione corrente di C ++) che non menziona i punti della sequenza. Sfortunatamente non sono abbastanza informato sulla sostituzione dei punti di sequenza in C ++ 11.
CB Bailey

3
Se non fosse indefinito, sicuramente non potrebbe essere 10, sarebbe o 01o 00. ( c++restituirà sempre il valore che caveva prima di essere incrementato). E anche se non fosse indefinito sarebbe comunque terribilmente confuso.
sinistra intorno

2
Sai, quando ho letto il titolo "cout << c ++ << c", l'ho pensato momentaneamente come un'affermazione sulla relazione tra i linguaggi C e C ++, e qualche altro chiamato "cout". Sai, come se qualcuno stesse dicendo come pensavano che "cout" fosse molto inferiore a C ++, e che C ++ fosse molto inferiore a C - e probabilmente per transitività quel "cout" era molto, molto inferiore a C. :)
tchrist

Risposte:


145

Puoi pensare a:

cout << a++ << a;

Come:

std::operator<<(std::operator<<(std::cout, a++), a);

C ++ garantisce che tutti gli effetti collaterali delle valutazioni precedenti saranno stati eseguiti nei punti della sequenza . Non ci sono punti di sequenza tra la valutazione degli argomenti della funzione, il che significa che l'argomento apuò essere valutato prima dell'argomento std::operator<<(std::cout, a++)o dopo. Quindi il risultato di quanto sopra è indefinito.


Aggiornamento C ++ 17

In C ++ 17 le regole sono state aggiornate. In particolare:

In un'espressione dell'operatore di spostamento E1<<E2e E1>>E2, ogni calcolo di valore ed effetto collaterale di E1viene sequenziato prima di ogni calcolo di valore ed effetto collaterale di E2.

Ciò significa che richiede il codice per produrre il risultato b, che restituisce 01.

Per ulteriori dettagli, vedere P0145R3 Raffinamento dell'ordine di valutazione delle espressioni per C ++ idiomatico .


@ Maxim: grazie per l'espansione. Con le chiamate che hai spiegato sarebbe un comportamento indefinito. Ma ora, ho un'altra domanda (potrebbe essere una domanda stupida, e mi manca qualcosa di base e penso ad alta voce) Come hai dedotto che la versione globale di std :: operator << () verrebbe chiamata invece di ostream :: operator < <() versione membro. Durante il debug sto atterrando in una versione membro di ostream :: operator << () chiamata piuttosto che in versione globale e questo è il motivo per cui inizialmente pensavo che la risposta sarebbe stata 01
pravs

@ Maxim Non che ne faccia una diversa, ma poiché cha il tipo int, operator<<ecco le funzioni membro.
James Kanze

2
@pravs: se operator<<è una funzione membro o una funzione indipendente non influisce sui punti della sequenza.
Maxim Egorushkin

11
Il "punto sequenza" non è più utilizzato nello standard C ++. Era impreciso ed è stato sostituito con la relazione "in sequenza prima / in sequenza dopo".
Rafał Dowgird

2
So the result of the above is undefined.La tua spiegazione è valida solo per non specificato , non per non definito . JamesKanze ha spiegato come sia ancora più indefinito nella sua risposta .
Deduplicatore

68

Tecnicamente, nel complesso questo è un comportamento indefinito .

Ma ci sono due aspetti importanti nella risposta.

La dichiarazione in codice:

std::cout << a++ << a;

è valutato come:

std::operator<<(std::operator<<(std::cout, a++), a);

Lo standard non definisce l'ordine di valutazione degli argomenti di una funzione.
Quindi O:

  • std::operator<<(std::cout, a++) viene valutato prima o
  • aviene valutato prima o
  • potrebbe essere qualsiasi ordine definito dall'implementazione.

Questo ordine non è specificato [Rif 1] secondo lo standard.

[Rif 1] C ++ 03 5.2.2 Chiamata di funzione,
paragrafo 8

L'ordine di valutazione degli argomenti non è specificato . Tutti gli effetti collaterali delle valutazioni delle espressioni degli argomenti hanno effetto prima che la funzione venga inserita. L'ordine di valutazione dell'espressione postfissa e l'elenco delle espressioni degli argomenti non è specificato.

Inoltre, non vi è alcun punto di sequenza tra la valutazione degli argomenti di una funzione, ma esiste un punto di sequenza solo dopo la valutazione di tutti gli argomenti [Rif 2] .

[Rif 2] C ++ 03 1.9 Esecuzione del programma [intro.execution]:
Par. 17:

Quando si chiama una funzione (indipendentemente dal fatto che la funzione sia inline o meno), c'è un punto di sequenza dopo la valutazione di tutti gli argomenti della funzione (se presenti) che ha luogo prima dell'esecuzione di qualsiasi espressione o istruzione nel corpo della funzione.

Si noti che, qui csi accede al valore di più di una volta senza un punto di sequenza intermedio, a questo proposito lo standard dice:

[Rif 3] C ++ 03 5 Espressioni [expr]:
Par. 4:

....
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 .

Il codice viene modificato cpiù di una volta senza intervenire sul punto della sequenza e non è possibile accedervi per determinare il valore dell'oggetto memorizzato. Questa è una chiara violazione della clausola di cui sopra e quindi il risultato come richiesto dallo standard è un comportamento indefinito [Rif 3] .


1
Tecnicamente, il comportamento è indefinito, perché c'è la modifica di un oggetto e l'accesso ad esso altrove senza un punto di sequenza intermedio. Undefined non è non specificato; lascia l'implementazione ancora più margine di manovra.
James Kanze

1
@Als Sì. Non avevo visto le tue modifiche (anche se stavo reagendo all'affermazione di jrok che il programma non può fare qualcosa di strano --- può). La tua versione modificata è buona fin dove va, ma nella mia mente, la parola chiave è l' ordinamento parziale ; i punti di sequenza introducono solo un ordinamento parziale.
James Kanze

1
@Alle grazie per una descrizione elaborata, davvero molto utile !!
pravs

4
Il nuovo standard C ++ 0x dice essenzialmente lo stesso ma in diverse sezioni e in diverse formule :) Quote: (1.9 Program Execution [intro.execution], par 15): "Se un effetto collaterale su un oggetto scalare non è sequenziato rispetto a o un altro effetto collaterale sullo stesso oggetto scalare o un calcolo del valore utilizzando il valore dello stesso oggetto scalare, il comportamento è indefinito. "
Rafał Dowgird

2
Credo che ci sia un bug in questa risposta. "std :: cout << c ++ << c;" non può essere tradotto in "std :: operator << (std :: operator << (std :: cout, c ++), c)", perché std :: operator << (std :: ostream &, int) non esiste. Invece, si traduce in "std :: cout.operator << (c ++). Operator (c);", che in realtà ha un punto di sequenza tra la valutazione di "c ++" e "c" (un operatore sovraccarico è considerato un chiamata di funzione e quindi c'è un punto di sequenza quando la chiamata di funzione ritorna). Di conseguenza viene specificato il comportamento e l'ordine di esecuzione .
Christopher Smith,

20

I punti di sequenza definiscono solo un ordinamento parziale . Nel tuo caso, hai (una volta terminata la risoluzione del sovraccarico):

std::cout.operator<<( a++ ).operator<<( a );

C'è un punto sequenza tra la a++e la prima chiamata a std::ostream::operator<<, e c'è un punto sequenza tra la seconda ae la seconda chiamata a std::ostream::operator<<, ma non c'è alcun punto sequenza tra a++e a; gli unici vincoli di ordinamento sono che devono a++essere valutati completamente (inclusi gli effetti collaterali) prima della prima chiamata a operator<<e che il secondo deve aessere valutato completamente prima della seconda chiamata a operator<<. (Esistono anche vincoli di ordinamento causale: la seconda chiamata a operator<<non può precedere la prima, poiché richiede i risultati della prima come argomento.) §5 / 4 (C ++ 03) afferma:

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.

Uno degli ordinamenti ammissibili della tua espressione è a++, a, prima chiamata a operator<<, seconda chiamata a operator<<; questo modifica il valore memorizzato di a( a++) e vi accede se non per determinare il nuovo valore (il secondo a), il comportamento non è definito.


Una presa dalla tua citazione dello standard. Il "tranne dove indicato", IIRC, include un'eccezione quando si tratta di un operatore sovraccarico, che tratta l'operatore come una funzione e quindi crea un punto di sequenza tra la prima e la seconda chiamata a std :: ostream :: operator << (int ). Perfavore, correggimi se sbaglio.
Christopher Smith,

@ChristopherSmith Un operatore sovraccarico si comporta come una chiamata di funzione. Se cfosse un tipo di utente con un utente definito ++, invece di int, i risultati sarebbero non specificati, ma non ci sarebbe alcun comportamento indefinito.
James Kanze

1
@ChristopherSmith Dove si vede un punto di sequenza tra i due cin foo(foo(bar(c)), c)? C'è un punto di sequenza quando le funzioni vengono chiamate e quando ritornano, ma non è richiesta alcuna chiamata di funzione tra le valutazioni delle due c.
James Kanze

1
@ChristopherSmith Se cfosse un UDT, gli operatori sovraccaricati sarebbero chiamate di funzione e introdurrebbero un punto sequenza, quindi il comportamento non sarebbe indefinito. Ma sarebbe ancora non specificato se la sottoespressione è cstata valutata prima o dopo c++, quindi se hai ottenuto la versione incrementata o meno non sarebbe specificato (e in teoria, non dovrebbe essere la stessa ogni volta).
James Kanze

1
@ChristopherSmith Tutto prima del punto della sequenza accadrà prima di qualsiasi cosa dopo il punto della sequenza. Ma i punti di sequenza definiscono solo un ordinamento parziale. Nell'espressione in questione, ad esempio, non esiste un punto di sequenza tra le sottoespressioni ce c++, quindi le due possono verificarsi in qualsiasi ordine. Per quanto riguarda i punti e virgola ... Causano un punto di sequenza solo nella misura in cui sono espressioni complete. Altri punti di sequenza importanti sono la chiamata di funzione: f(c++)vedrà il incrementato cin f, e l'operatore virgola &&, ||e ?:anche causare punti di sequenza.
James Kanze

4

La risposta corretta è mettere in discussione la domanda. L'affermazione è inaccettabile perché un lettore non può vedere una risposta chiara. Un altro modo per vederlo è che abbiamo introdotto effetti collaterali (c ++) che rendono l'affermazione molto più difficile da interpretare. Il codice conciso è ottimo, a condizione che il significato sia chiaro.


4
La domanda potrebbe mostrare una scarsa pratica di programmazione (e persino un C ++ non valido). Ma una risposta dovrebbe rispondere alla domanda che indica cosa è sbagliato e perché è sbagliato. Un commento alla domanda non è una risposta anche se sono perfettamente valide. Nella migliore delle ipotesi, questo può essere un commento, non una risposta.
PP
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.