Perché f (i = -1, i = -1) comportamento indefinito?


267

Stavo leggendo sull'ordine delle violazioni della valutazione e mi danno un esempio che mi confonde.

1) Se un effetto collaterale su un oggetto scalare non è sequenziato rispetto ad un altro effetto collaterale sullo stesso oggetto scalare, il comportamento non è definito.

// snip
f(i = -1, i = -1); // undefined behavior

In questo contesto, iè un oggetto scalare , che apparentemente significa

I tipi aritmetici (3.9.1), i tipi di enumerazione, i tipi di puntatore, i tipi di puntatore a membro (3.9.2), le versioni std :: nullptr_t e cv qualificati di questi tipi (3.9.3) sono chiamati collettivamente tipi scalari.

Non vedo come la dichiarazione sia ambigua in quel caso. Mi sembra che, indipendentemente dal fatto che il primo o il secondo argomento vengano valutati per primi, ifinisce come -1, e lo sono anche entrambi gli argomenti -1.

Qualcuno può chiarire per favore?


AGGIORNARE

Apprezzo molto tutta la discussione. Finora, mi piace molto la risposta di @ harmic poiché espone le insidie ​​e le complessità della definizione di questa affermazione, nonostante quanto a prima vista appaia semplice. @ acheong87 sottolinea alcuni problemi che emergono quando si usano i riferimenti, ma penso che sia ortogonale all'aspetto senza effetti collaterali di questa domanda.


SOMMARIO

Dal momento che questa domanda ha ricevuto molta attenzione, riassumerò i principali punti / risposte. In primo luogo, consentitemi una piccola digressione per sottolineare che "perché" può avere significati strettamente correlati ma sottilmente diversi, vale a dire "per quale causa ", "per quale motivo " e "per quale scopo ". Raggrupperò le risposte per quale di questi significati di "perché" hanno affrontato.

per quale causa

La risposta principale qui viene da Paul Draper , con Martin J che contribuisce con una risposta simile ma non così ampia. La risposta di Paul Draper si riduce a

È un comportamento indefinito perché non è definito quale sia il comportamento.

La risposta è nel complesso molto buona in termini di spiegazione di ciò che dice lo standard C ++. Affronta anche alcuni casi correlati di UB come f(++i, ++i);e f(i=1, i=-1);. Nel primo dei casi correlati, non è chiaro se il primo argomento debba essere i+1e il secondo i+2o viceversa; nel secondo, non è chiaro se idovrebbe essere 1 o -1 dopo la chiamata di funzione. Entrambi questi casi sono UB perché rientrano nella seguente regola:

Se un effetto collaterale su un oggetto scalare non è seguito rispetto ad un altro effetto collaterale sullo stesso oggetto scalare, il comportamento non è definito.

Pertanto, f(i=-1, i=-1)è anche UB poiché rientra nella stessa regola, nonostante l'intenzione del programmatore sia (IMHO) ovvia e inequivocabile.

Paul Draper lo rende anche esplicito nelle sue conclusioni

Potrebbe essere stato definito un comportamento? Sì. È stato definito? No.

che ci porta alla domanda "per quale motivo / scopo è stato f(i=-1, i=-1)lasciato come comportamento indefinito?"

per quale motivo / scopo

Sebbene ci siano alcune sviste (forse trascurate) nello standard C ++, molte omissioni sono ben ragionate e servono a uno scopo specifico. Sebbene sia consapevole che lo scopo è spesso "rendere più facile il lavoro dello scrittore del compilatore" o "codice più veloce", ero principalmente interessato a sapere se c'è una buona ragione per lasciare f(i=-1, i=-1) UB.

harmic e supercat forniscono le risposte principali che forniscono un motivo per l'UB. Harmic sottolinea che un compilatore ottimizzante che potrebbe spezzare le apparentemente atomiche operazioni di assegnazione in più istruzioni della macchina e che potrebbe ulteriormente intercalare quelle istruzioni per una velocità ottimale. Questo potrebbe portare ad alcuni risultati molto sorprendenti: ifinisce come -2 nel suo scenario! Pertanto, harmic dimostra come l'assegnazione dello stesso valore a una variabile più di una volta può avere effetti negativi se le operazioni non sono seguite.

supercat fornisce un'esposizione correlata delle insidie ​​del tentativo di arrivare f(i=-1, i=-1)a fare quello che sembra che dovrebbe fare. Sottolinea che su alcune architetture ci sono forti restrizioni contro più scritture simultanee nello stesso indirizzo di memoria. Un compilatore potrebbe avere difficoltà a coglierlo se avessimo a che fare con qualcosa di meno banale di f(i=-1, i=-1).

davidf fornisce anche un esempio di istruzioni interleaving molto simili a quelle degli harmic.

Sebbene ciascuno degli esempi di harmic, supercat e davidf sia in qualche modo inventato, presi insieme servono comunque a fornire una ragione tangibile per cui f(i=-1, i=-1)dovrebbe essere un comportamento indefinito.

Ho accettato la risposta di harmic perché ha fatto il miglior lavoro per affrontare tutti i significati del perché, anche se la risposta di Paul Draper ha affrontato meglio la parte "per quale causa".

altre risposte

JohnB sottolinea che se consideriamo operatori di assegnazione sovraccaricati (anziché solo semplici scalari), allora potremmo anche avere problemi.


1
Un oggetto scalare è un oggetto di tipo scalare. Vedi 3.9 / 9: "I tipi aritmetici (3.9.1), i tipi di enumerazione, i tipi di puntatore, i puntatori ai tipi di membri (3.9.2) std::nullptr_te le versioni qualificate per cv di questi tipi (3.9.3) sono chiamati collettivamente tipi scalari . "
Rob Kennedy,

1
Forse c'è un errore nella pagina e in realtà significano f(i-1, i = -1)o qualcosa di simile.
Lister,

Dai un'occhiata a questa domanda: stackoverflow.com/a/4177063/71074
Robert S. Barnes,

@RobKennedy Grazie. I "tipi aritmetici" includono il bool?
Nicu Stiurca,

1
SchighSchagh l'aggiornamento dovrebbe essere nella sezione delle risposte.
Grijesh Chauhan,

Risposte:


343

Poiché le operazioni non sono conseguite, non c'è nulla da dire che le istruzioni che eseguono l'assegnazione non possono essere intercalate. Potrebbe essere ottimale farlo, a seconda dell'architettura della CPU. La pagina di riferimento indica questo:

Se A non è sequenziato prima di B e B non è sequenziato prima di A, allora esistono due possibilità:

  • le valutazioni di A e B non sono seguite: possono essere eseguite in qualsiasi ordine e possono sovrapporsi (all'interno di un singolo thread di esecuzione, il compilatore può intercalare le istruzioni della CPU che comprendono A e B)

  • le valutazioni di A e B hanno una sequenza indeterminata: possono essere eseguite in qualsiasi ordine ma non possono sovrapporsi: o A sarà completato prima di B, oppure B sarà completato prima di A. L'ordine potrebbe essere l'opposto la prossima volta che la stessa espressione viene valutato.

Questo da solo non sembra causare un problema - supponendo che l'operazione in corso stia memorizzando il valore -1 in una posizione di memoria. Ma non c'è nemmeno nulla da dire che il compilatore non può ottimizzarlo in un set separato di istruzioni che ha lo stesso effetto, ma che potrebbe fallire se l'operazione fosse interfogliata con un'altra operazione nella stessa posizione di memoria.

Ad esempio, immagina che sia stato più efficiente azzerare la memoria, quindi diminuirla, rispetto al caricamento del valore -1 in. Quindi questo:

f(i=-1, i=-1)

potrebbe diventare:

clear i
clear i
decr i
decr i

Ora sono -2.

È probabilmente un esempio falso, ma è possibile.


59
Bell'esempio di come l'espressione potrebbe effettivamente fare qualcosa di inaspettato mentre si conforma alle regole di sequenziamento. Sì, un po 'forzato, ma lo è anche il codice spezzato di cui sto chiedendo. :)
Nicu Stiurca,

10
E anche se l'assegnazione viene eseguita come un'operazione atomica, è possibile concepire un'architettura superscalare in cui entrambe le assegnazioni vengono eseguite contemporaneamente causando un conflitto di accesso alla memoria che provoca un errore. Il linguaggio è progettato in modo tale che gli autori di compilatori abbiano la massima libertà possibile nell'utilizzare i vantaggi della macchina target.
ach

11
Mi piace molto il tuo esempio di come anche l'assegnazione dello stesso valore alla stessa variabile in entrambi i parametri potrebbe comportare un risultato imprevisto perché le due assegnazioni non sono seguite
Martin J.

1
+ 1e + 6 (ok, +1) per il punto in cui il codice compilato non è sempre quello che ti aspetteresti. Gli ottimizzatori sono davvero bravi a lanciarti questo tipo di curve quando non segui le regole: P
Corey,

3
Sul processore Arm un carico a 32 bit può richiedere fino a 4 istruzioni: lo fa load 8bit immediate and shiftfino a 4 volte. Di solito il compilatore eseguirà l'indirizzamento indiretto per recuperare un numero da una tabella per evitarlo. (-1 può essere fatto in 1 istruzione, ma è possibile scegliere un altro esempio).
ctrl-alt-delor,

208

Innanzitutto, "oggetto scalare" significa un tipo come un int, floato un puntatore (vedi Cos'è un oggetto scalare in C ++? ).


In secondo luogo, può sembrare più ovvio che

f(++i, ++i);

avrebbe un comportamento indefinito. Ma

f(i = -1, i = -1);

è meno ovvio.

Un esempio leggermente diverso:

int i;
f(i = 1, i = -1);
std::cout << i << "\n";

Quale compito è successo "ultimo" i = 1, oppure i = -1? Non è definito nello standard. In realtà, ciò significa che ipotrebbe essere 5(vedi la risposta di harmic per una spiegazione completamente plausibile di come questo dovrebbe essere il caso). Oppure il programma potrebbe essere segfault. O riformatta il tuo disco rigido.

Ma ora mi chiedi: "E il mio esempio? Ho usato lo stesso valore ( -1) per entrambi i compiti. Cosa potrebbe non essere chiaro a riguardo?"

Hai ragione ... tranne nel modo in cui il comitato degli standard C ++ lo ha descritto.

Se un effetto collaterale su un oggetto scalare non è seguito rispetto ad un altro effetto collaterale sullo stesso oggetto scalare, il comportamento non è definito.

Avrebbero potuto fare un'eccezione speciale per il tuo caso speciale, ma non lo fecero. (E perché dovrebbero? Che utilità avrebbe mai potuto avere?) Quindi, ipotrebbe ancora essere 5. O il tuo disco rigido potrebbe essere vuoto. Quindi la risposta alla tua domanda è:

È un comportamento indefinito perché non è definito quale sia il comportamento.

(Ciò merita enfasi perché molti programmatori pensano che "indefinito" significhi "casuale" o "imprevedibile". Non; significa che non è definito dallo standard. Il comportamento potrebbe essere coerente al 100% e comunque non definito.)

Potrebbe essere stato definito un comportamento? Sì. È stato definito? No. Quindi, è "indefinito".

Detto questo, "indefinito" non significa che un compilatore formatterà il tuo disco rigido ... significa che potrebbe e sarebbe comunque un compilatore conforme agli standard. Realisticamente, sono sicuro che g ++, Clang e MSVC faranno tutti quello che ti aspettavi. Semplicemente non avrebbero "dovuto".


Una domanda diversa potrebbe essere: perché il comitato per gli standard C ++ ha scelto di non dare seguito a questo effetto collaterale? . Tale risposta coinvolgerà la storia e le opinioni del comitato. O cosa c'è di buono nell'avere questo effetto collaterale senza conseguenze in C ++? , che consente qualsiasi giustificazione, indipendentemente dal fatto che si tratti o meno del ragionamento effettivo del comitato per le norme. Puoi porre queste domande qui o su programmers.stackexchange.com.


9
@hvd, sì, infatti so che se abiliti -Wsequence-pointper g ++, ti avvertirà.
Paul Draper,

47
"Sono sicuro che g ++, Clang e MSVC faranno tutti quello che ti aspettavi" Non mi fiderei di un compilatore moderno. Sono cattivi. Ad esempio, potrebbero riconoscere che si tratta di un comportamento indefinito e presumere che questo codice non sia raggiungibile. Se non lo fanno oggi, potrebbero farlo domani. Qualsiasi UB è una bomba a orologeria.
CodesInChaos,

8
@BlacklightShining "la tua risposta è negativa perché non è buona" non è un feedback molto utile, vero?
Vincent van der Weele,

13
@BobJarvis La compilazione non ha assolutamente l'obbligo di generare codici anche remotamente corretti di fronte a comportamenti indefiniti. Si può anche presumere che questo codice non sia mai nemmeno chiamato e quindi sostituire il tutto con un nop (si noti che i compilatori in realtà fanno simili assunzioni di fronte a UB). Pertanto direi che la reazione corretta a tale segnalazione di bug può essere solo "chiusa, funziona come previsto"
Grizzly,

7
@SchighSchagh A volte una riformulazione dei termini (che solo in superficie sembra essere una risposta tautologica) è ciò di cui le persone hanno bisogno. La maggior parte delle persone che non conoscono le specifiche tecniche pensano che ciò undefined behaviorsignifichi something random will happen, il che è tutt'altro che vero.
Izkata,

27

Un motivo pratico per non fare un'eccezione alle regole solo perché i due valori sono uguali:

// config.h
#define VALUEA  1

// defaults.h
#define VALUEB  1

// prog.cpp
f(i = VALUEA, i = VALUEB);

Considera il caso questo è stato permesso.

Ora, alcuni mesi dopo, sorge la necessità di cambiare

 #define VALUEB 2

Apparentemente innocuo, vero? Eppure improvvisamente prog.cpp non si sarebbe più compilato. Tuttavia, riteniamo che la compilazione non dovrebbe dipendere dal valore di un letterale.

In conclusione: non c'è eccezione alla regola perché farebbe in modo che la compilazione corretta dipenda dal valore (piuttosto dal tipo) di una costante.

MODIFICARE

@HeartWare ha sottolineato che espressioni costanti del modulo A DIV Bnon sono consentite in alcune lingue, quando Bè 0, e causano il fallimento della compilazione. Quindi il cambio di una costante potrebbe causare errori di compilazione in qualche altro posto. Che è, IMHO, sfortunato. Ma è certamente bene limitare tali cose all'inevitabile.


Certo, ma l'esempio fa uso intero letterali. Il tuo f(i = VALUEA, i = VALUEB);ha sicuramente il potenziale per un comportamento indefinito. Spero che tu non stia davvero codificando contro valori dietro identificatori.
Lupo,

3
@Wold Ma il compilatore non vede le macro del preprocessore. E anche se non fosse così, è difficile trovare un esempio in qualsiasi linguaggio di programmazione, in cui un codice sorgente viene compilato fino a quando non si cambia una costante int da 1 a 2. Questo è semplicemente inaccettabile e inspiegabile, mentre qui vedete ottime spiegazioni perché quel codice è rotto anche con gli stessi valori.
Ingo,

Sì, la compilazione non vede le macro. Ma era questa la domanda?
Lupo,

1
Alla tua risposta manca il punto, leggi la risposta di harmic e il commento dell'OP su di essa.
Lupo,

1
Potrebbe fare SomeProcedure(A, B, B DIV (2-A)). Ad ogni modo, se la lingua afferma che CONST deve essere completamente valutato in fase di compilazione, ovviamente la mia richiesta non è valida per quel caso. Dal momento che in qualche modo confonde la distinzione tra compiletime e runtime. Noterebbe anche se scriviamo CONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y; ?? O le funzioni non sono consentite?
Ingo,

12

La confusione è che la memorizzazione di un valore costante in una variabile locale non è un'istruzione atomica su ogni architettura su cui C è progettato per essere eseguito. In questo caso, il processore su cui viene eseguito il codice è più importante del compilatore. Ad esempio, su ARM in cui ogni istruzione non può trasportare una costante completa a 32 bit, la memorizzazione di un int in una variabile richiede più di un'istruzione. Esempio con questo pseudo codice in cui è possibile memorizzare solo 8 bit alla volta e deve funzionare in un registro a 32 bit, i è un int32:

reg = 0xFF; // first instruction
reg |= 0xFF00; // second
reg |= 0xFF0000; // third
reg |= 0xFF000000; // fourth
i = reg; // last

Puoi immaginare che se il compilatore vuole ottimizzare, può intercalare due volte la stessa sequenza e non sai quale valore verrà scritto su i; e diciamo che non è molto intelligente:

reg = 0xFF;
reg |= 0xFF00;
reg |= 0xFF0000;
reg = 0xFF;
reg |= 0xFF000000;
i = reg; // writes 0xFF0000FF == -16776961
reg |= 0xFF00;
reg |= 0xFF0000;
reg |= 0xFF000000;
i = reg; // writes 0xFFFFFFFF == -1

Comunque nei miei test gcc è abbastanza gentile da riconoscere che lo stesso valore viene usato due volte e lo genera una volta e non fa nulla di strano. Ottengo -1, -1 Ma il mio esempio è ancora valido in quanto è importante considerare che anche una costante potrebbe non essere così ovvia come sembra.


Suppongo che su ARM il compilatore caricherà semplicemente la costante da una tabella. Quello che descrivi sembra più un MIPS.
ach

1
@AndreyChernyakhovskiy Sì, ma in un caso in cui non è semplicemente -1(che il compilatore ha archiviato da qualche parte), ma è piuttosto 3^81 mod 2^32, ma costante, quindi il compilatore potrebbe fare esattamente ciò che viene fatto qui, e in qualche leva di ottimizzazione, il mio interlacciare le sequenze di chiamata per evitare di aspettare.
giovedì

@tohecz, sì, l'ho già verificato. In effetti, il compilatore è troppo intelligente per caricare ogni costante da una tabella. Comunque, non userebbe mai lo stesso registro per calcolare le due costanti. Ciò significherebbe sicuramente "indefinire" il comportamento definito.
ach

@AndreyChernyakhovskiy Ma probabilmente non sei "tutti i programmatori di compilatori C ++ nel mondo". Ricorda che ci sono macchine con 3 registri brevi disponibili solo per i calcoli.
giovedì

@tohecz, considera l'esempio in f(i = A, j = B)cui ie jsono due oggetti separati. Questo esempio non ha UB. La macchina con 3 registri corti non è una scusa per il compilatore di mescolare i due valori di Ae Bnello stesso registro (come mostrato nella risposta di @ davidf), perché interromperebbe la semantica del programma.
ach

11

Il comportamento è comunemente definito come indefinito se c'è qualche ragione plausibile per cui un compilatore che stava cercando di essere "utile" potrebbe fare qualcosa che causerebbe un comportamento totalmente inaspettato.

Nel caso in cui una variabile venga scritta più volte senza nulla per garantire che le scritture avvengano in momenti distinti, alcuni tipi di hardware potrebbero consentire l'esecuzione simultanea di più operazioni di "archiviazione" su indirizzi diversi utilizzando una memoria a doppia porta. Tuttavia, alcune memorie a doppia porta vietano espressamente lo scenario in cui due negozi raggiungono lo stesso indirizzo contemporaneamente, indipendentemente dal fatto che i valori scritti corrispondano o meno. Se un compilatore per tale macchina rileva due tentativi non previsti di scrivere la stessa variabile, potrebbe rifiutarsi di compilare o assicurarsi che le due scritture non possano essere programmate contemporaneamente. Ma se uno o entrambi gli accessi sono tramite un puntatore o un riferimento, il compilatore potrebbe non essere sempre in grado di dire se entrambe le scritture potrebbero colpire la stessa posizione di archiviazione. In tal caso, potrebbe pianificare le scritture contemporaneamente, causando una trappola hardware nel tentativo di accesso.

Naturalmente, il fatto che qualcuno possa implementare un compilatore C su una tale piattaforma non suggerisce che tale comportamento non debba essere definito su piattaforme hardware quando si utilizzano negozi di tipi abbastanza piccoli da essere elaborati atomicamente. Cercare di memorizzare due diversi valori in modo non seguito potrebbe causare stranezze se un compilatore non ne è consapevole; ad esempio, dato:

uint8_t v;  // Global

void hey(uint8_t *p)
{
  moo(v=5, (*p)=6);
  zoo(v);
  zoo(v);
}

se il compilatore allinea la chiamata a "moo" e può dire che non modifica "v", potrebbe memorizzare un 5 a v, quindi memorizzare un 6 a * p, quindi passare 5 a "zoo" e quindi passare il contenuto di v a "zoo". Se "zoo" non modifica "v", le due chiamate non dovrebbero in alcun modo passare valori diversi, ma ciò potrebbe facilmente avvenire comunque. D'altra parte, nei casi in cui entrambi i negozi scrivessero lo stesso valore, tale stranezza non potrebbe verificarsi e sulla maggior parte delle piattaforme non ci sarebbe una ragione ragionevole per un'implementazione di fare qualcosa di strano. Sfortunatamente, alcuni autori di compilatori non hanno bisogno di scuse per comportamenti sciocchi oltre "perché lo standard lo consente", quindi anche quei casi non sono sicuri.


9

Il fatto che il risultato sarebbe lo stesso nella maggior parte delle implementazioni in questo caso è casuale; l'ordine di valutazione è ancora indefinito. Considera f(i = -1, i = -2): qui, l'ordine conta. L'unico motivo per cui non importa nel tuo esempio è l'incidente che entrambi i valori sono -1.

Dato che l'espressione è specificata come una con un comportamento indefinito, un compilatore pericolosamente conforme potrebbe visualizzare un'immagine inappropriata quando si valuta f(i = -1, i = -1)e si interrompe l'esecuzione - ed è comunque considerato completamente corretto. Fortunatamente, nessun compilatore di cui sono a conoscenza lo fa.


8

Mi sembra che l'unica regola relativa al sequenziamento dell'espressione dell'argomento della funzione sia qui:

3) Quando si chiama una funzione (indipendentemente dal fatto che la funzione sia in linea o meno e che venga utilizzata o meno la sintassi della chiamata di funzione esplicita), ogni calcolo di valore ed effetto collaterale associato a qualsiasi espressione di argomento o all'espressione postfissa che designa la funzione chiamata, è sequenziato prima dell'esecuzione di ogni espressione o istruzione nel corpo della funzione chiamata.

Questo non definisce il sequenziamento tra le espressioni degli argomenti, quindi finiamo in questo caso:

1) Se un effetto collaterale su un oggetto scalare non è seguito rispetto ad un altro effetto collaterale sullo stesso oggetto scalare, il comportamento non è definito.

In pratica, sulla maggior parte dei compilatori, l'esempio che hai citato andrà bene (al contrario di "cancellare il tuo disco rigido" e altre conseguenze teoriche di comportamento indefinito).
È, tuttavia, una responsabilità, poiché dipende dal comportamento specifico del compilatore, anche se i due valori assegnati sono gli stessi. Inoltre, ovviamente, se provassi ad assegnare valori diversi, i risultati sarebbero "veramente" indefiniti:

void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}

8

C ++ 17 definisce regole di valutazione più rigorose. In particolare, sequenzia gli argomenti delle funzioni (sebbene in ordine non specificato).

N5659 §4.6:15
Le valutazioni A e B sono indeterminate in sequenza quando una delle sequenze A viene eseguita prima della sequenza B o B prima di A , ma non è specificato quale. [ Nota : le valutazioni in sequenza indeterminata non possono sovrapporsi, ma entrambe possono essere eseguite per prime. - nota finale ]

N5659 § 8.2.2:5
L'inizializzazione di un parametro, inclusi tutti i calcoli dei valori associati e gli effetti collaterali, è indeterminata in sequenza rispetto a quella di qualsiasi altro parametro.

Permette alcuni casi che sarebbero UB prima:

f(i = -1, i = -1); // value of i is -1
f(i = -1, i = -2); // value of i is either -1 or -2, but not specified which one

2
Grazie per aver aggiunto questo aggiornamento per c ++ 17 , quindi non ho dovuto. ;)
Yakk - Adam Nevraumont,

Fantastico, grazie mille per questa risposta. Leggero follow-up: se fla firma fosse f(int a, int b), C ++ 17 lo garantisce a == -1e b == -2se chiamato come nel secondo caso?
Nicu Stiurca,

Sì. Se disponiamo di parametri ae b, quindi, i-then- aviene inizializzato su -1, successivamente i-then- bviene inizializzato su -2 o viceversa. In entrambi i casi, finiamo con a == -1e b == -2. Almeno questo è il modo in cui ho letto " L'inizializzazione di un parametro, inclusi tutti i calcoli dei valori associati e gli effetti collaterali, è indeterminata in sequenza rispetto a quella di qualsiasi altro parametro ".
AlexD,

Penso che sia stato lo stesso in C da sempre.
fuz,

5

L'operatore di assegnazione potrebbe essere sovraccarico, nel qual caso l'ordine potrebbe importare:

struct A {
    bool first;
    A () : first (false) {
    }
    const A & operator = (int i) {
        first = !first;
        return * this;
    }
};

void f (A a1, A a2) {
    // ...
}


// ...
A i;
f (i = -1, i = -1);   // the argument evaluated first has ax.first == true

1
Abbastanza vero, ma la domanda era sui tipi scalari , che altri hanno sottolineato significa essenzialmente int family, float family e pointers.
Nicu Stiurca,

Il vero problema in questo caso è che l'operatore di assegnazione è con stato, quindi anche una manipolazione regolare della variabile è soggetta a problemi come questo.
AJMansfield,

2

Questo è solo rispondere al "Non sono sicuro di cosa significhi" oggetto scalare "oltre a qualcosa come un int o un float".

Interpreterei "oggetto scalare" come un'abbreviazione di "oggetto di tipo scalare", o semplicemente "variabile di tipo scalare". Poi, pointer, enum(costante) sono di tipo scalare.

Questo è un articolo MSDN sui tipi scalari .


Questo sembra un po 'come una "risposta solo link". Riesci a copiare i bit rilevanti da quel link a questa risposta (in una citazione in blocco)?
Cole Johnson,

1
@ColeJohnson Questa non è solo una risposta al link. Il link è solo per ulteriori spiegazioni. La mia risposta è "pointer", "enum".
Peng Zhang,

Non ho detto che la tua risposta fosse solo un link. Ho detto che "legge come [uno]" . Ti suggerisco di leggere perché non desideriamo solo le risposte dei link nella sezione di aiuto. Il motivo è che, se Microsoft aggiorna i propri URL nel proprio sito, il collegamento si interrompe.
Cole Johnson,

1

In realtà, c'è un motivo per non dipendere dal fatto che il compilatore verificherà che isia assegnato lo stesso valore due volte, in modo che sia possibile sostituirlo con una singola assegnazione. E se avessimo delle espressioni?

void g(int a, int b, int c, int n) {
    int i;
    // hey, compiler has to prove Fermat's theorem now!
    f(i = 1, i = (ipow(a, n) + ipow(b, n) == ipow(c, n)));
}

1
Non c'è bisogno di provare il teorema di Fermat: basta assegnare 1a i. Entrambi gli argomenti assegnano 1e questo fa la cosa "giusta", oppure gli argomenti assegnano valori diversi ed è un comportamento indefinito, quindi la nostra scelta è ancora consentita.
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.