Perché l'operatore ternario con virgole valuta solo un'espressione nel caso vero?


119

Attualmente sto imparando C ++ con il libro C ++ Primer e uno degli esercizi nel libro è:

Spiega cosa fa la seguente espressione: someValue ? ++x, ++y : --x, --y

Cosa sappiamo? Sappiamo che l'operatore ternario ha una precedenza maggiore dell'operatore virgola. Con gli operatori binari questo era abbastanza facile da capire, ma con l'operatore ternario sto faticando un po '. Con operatori binari "con precedenza maggiore" significa che possiamo usare parentesi attorno all'espressione con precedenza maggiore e non cambierà l'esecuzione.

Per l'operatore ternario farei:

(someValue ? ++x, ++y : --x, --y)

risultando effettivamente nello stesso codice che non mi aiuta a capire come il compilatore raggrupperà il codice.

Tuttavia, dal test con un compilatore C ++ so che l'espressione si compila e non so cosa :possa rappresentare un operatore da solo. Quindi il compilatore sembra interpretare correttamente l'operatore ternario.

Quindi ho eseguito il programma in due modi:

#include <iostream>

int main()
{
    bool someValue = true;
    int x = 10, y = 10;

    someValue ? ++x, ++y : --x, --y;

    std::cout << x << " " << y << std::endl;
    return 0;
}

Risultati in:

11 10

Mentre d'altra parte con someValue = falseesso stampa:

9 9

Perché il compilatore C ++ dovrebbe generare codice che per il ramo vero dell'operatore ternario incrementa solo x, mentre per il ramo falso del ternario decrementa sia xe y?

Sono persino arrivato a mettere parentesi attorno al vero ramo in questo modo:

someValue ? (++x, ++y) : --x, --y;

ma risulta comunque 11 10.


5
La "precedenza" è solo un fenomeno emergente in C ++. Potrebbe essere più semplice guardare direttamente la grammatica della lingua e vedere come funzionano le espressioni.
Kerrek SB

26
Non ci importa che molto circa i principi. :-) Il fatto che tu debba chiedere questo qui indica che il codice non passerà mai una revisione del codice da parte dei tuoi colleghi programmatori. Ciò rende la conoscenza di come funziona effettivamente meno che utile. A meno che tu non voglia partecipare all'Obfuscated C Contest , ovviamente.
Bo Persson

5
@BoPersson senza esempi come questo da cui imparare, i futuri revisori non impareranno mai perché dovrebbero rifiutarlo dalla produzione.
Leushenko

8
@Leushenko - I campanelli di avvertimento dovrebbero suonare comunque. Più incrementi e decrementi nella stessa istruzione (ding, ding, ding!). Un operatore ternario quando potresti usare if-else (ding, ding, ding!). Aspetta, quelle virgole sono il temuto operatore virgola? (ding, DING, DING!) Con tutti quegli operatori, potrebbe esserci qualche cosa di precedenza? (ding, ding, ding!) Quindi non saremo mai in grado di usarlo. Allora perché perdere tempo a capire cosa fa, se non altro?
Bo Persson

4
Minor nit: il nome di ?è l' operatore condizionale . Il termine operatore ternario significa semplicemente un operatore con tre operandi. L'operatore condizionale è un esempio di operatore ternario, ma una lingua potrebbe (teoricamente) avere più operatori ternari.
bta

Risposte:


122

Come ha detto @Rakete nella sua eccellente risposta, questo è complicato. Vorrei aggiungere qualcosa a questo.

L'operatore ternario deve avere la forma:

logico-o-espressione ? espressione : assegnamento espressione

Quindi abbiamo le seguenti mappature:

  • someValue: logica-o-espressione
  • ++x, ++y: espressione
  • ??? è espressione-assegnazione --x, --y o solo --x?

In effetti è solo --xperché un'espressione di assegnazione non può essere analizzata come due espressioni separate da una virgola (secondo le regole grammaticali di C ++), quindi --x, --ynon può essere trattata come un'espressione di assegnazione .

Il che si traduce nella porzione di espressione ternaria (condizionale) simile a questa:

someValue?++x,++y:--x

Può essere utile per motivi di leggibilità considerare ++x,++ydi essere calcolato come se tra parentesi (++x,++y); qualsiasi cosa contenuta tra ?e :verrà sequenziata dopo il condizionale. (Li metterò tra parentesi per il resto del post).

e valutati in questo ordine:

  1. someValue?
  2. (++x,++y)o --x(a seconda del boolrisultato di 1.)

Questa espressione viene quindi trattata come la sottoespressione sinistra di un operatore virgola, con la sottoespressione destra --y, in questo modo:

(someValue?(++x,++y):--x), --y;

Il che significa che il lato sinistro è un'espressione di valore scartato , il che significa che è sicuramente valutato, ma poi valutiamo il lato destro e lo restituiamo.

Allora cosa succede quando someValueè true?

  1. (someValue?(++x,++y):--x)esegue e incrementa xe yessere 11e11
  2. L'espressione di sinistra viene scartata (anche se gli effetti collaterali dell'incremento rimangono)
  3. Valutiamo il lato destro dell'operatore virgola:, --yche poi decrementa di ynuovo in10

Per "correggere" il comportamento, puoi raggruppare --x, --ycon parentesi per trasformarlo in un'espressione primaria che è una voce valida per un'espressione di assegnazione *:

someValue?++x,++y:(--x, --y);

* È una lunga catena piuttosto divertente che collega un'espressione di assegnazione a un'espressione primaria:

Assegnazione-espressione --- (può essere costituito) -> condizionale-espressione -> logico-o-espressione -> logico-e-espressione -> inclusiva-o-espressione -> esclusiva-o-espressione - -> espressione-and -> espressione-uguaglianza -> espressione-relazionale -> espressione-shift -> espressione-additiva -> espressione-moltiplicativa -> espressione-pm -> espressione-cast -> espressione-unaria -> espressione-suffisso -> espressione-primaria


10
Grazie per esserti preso la briga di svelare le regole grammaticali; ciò mostra che c'è di più nella grammatica del C ++ di quanto troverai nella maggior parte dei libri di testo.
sdenham

4
@sdenham: Quando le persone chiedono perché i "linguaggi orientati alle espressioni" sono carini (cioè quando { ... }possono essere trattati come un'espressione), ora ho una risposta => è per evitare di dover introdurre un operatore virgola che si comporta in modi così complicati.
Matthieu M.

Potresti darmi un link per leggere sulla assignment-expressioncatena?
MiP

@MiP: ho sollevato dalla norma stessa, lo si può trovare sotto gram.expr
AndyG

88

Wow, è complicato.

Il compilatore vede la tua espressione come:

(someValue ? (++x, ++y) : --x), --y;

L'operatore ternario ha bisogno di una :, non può stare da solo in quel contesto, ma dopo di esso, non c'è motivo per cui la virgola dovrebbe appartenere al caso falso.

Ora potrebbe avere più senso il motivo per cui ottieni quell'output. Se someValueè vero, allora ++x, ++ye --yviene eseguito, il che non cambia effettivamente yma aggiunge uno a x.

Se someValueè falso, quindi --xe --yvengono eseguiti, entrambi decrementare di uno.


42

Perché il compilatore C ++ dovrebbe generare codice che per il ramo vero dell'operatore ternario aumenta solo x

Hai interpretato male quello che è successo. Il ramo vero incrementa sia xe y. Però,y viene decrementato immediatamente dopo, incondizionatamente.

Ecco come accade: poiché l'operatore condizionale ha una precedenza maggiore dell'operatore virgola in C ++ , il compilatore analizza l'espressione come segue:

   (someValue ? ++x, ++y : --x), (--y);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^  ^^^^^

Notare l '"orfano" --ydopo la virgola. Questo è ciò che porta al decremento yche è stato inizialmente incrementato.

Sono persino arrivato a mettere parentesi attorno al vero ramo in questo modo:

someValue ? (++x, ++y) : --x, --y;

Eri sulla strada giusta, ma hai inserito tra parentesi un ramo sbagliato: puoi rimediare inserendo tra parentesi il ramo else, in questo modo:

someValue ? ++x, ++y : (--x, --y);

Demo (stampe 11 11)


5

Il tuo problema è che l'espressione ternaria non ha realmente una precedenza maggiore della virgola. In effetti, il C ++ non può essere descritto accuratamente semplicemente in base alla precedenza - ed è esattamente l'interazione tra l'operatore ternario e la virgola che si rompe.

a ? b++, c++ : d++

è trattato come:

a ? (b++, c++) : d++

(la virgola si comporta come se avesse una precedenza maggiore). D'altro canto,

a ? b++ : c++, d++

è trattato come:

(a ? b++ : c++), d++

e l'operatore ternario ha la precedenza più alta.


Penso che questo sia ancora nel regno della precedenza poiché esiste solo un'analisi valida per la linea di mezzo, giusto? Tuttavia, è ancora un esempio utile
sudo rm -rf slash

2

Un punto che è stato trascurato nelle risposte (sebbene toccato nei commenti) è che l'operatore condizionale viene utilizzato invariabilmente (previsto dalla progettazione?) Nel codice reale come scorciatoia per assegnare uno dei due valori a una variabile.

Quindi, il contesto più ampio sarebbe:

whatIreallyWanted = someValue ? ++x, ++y : --x, --y;

Il che è assurdo in apparenza, quindi i crimini sono molteplici:

  • La lingua consente effetti collaterali ridicoli in un incarico.
  • Il compilatore non ti ha avvertito che stavi facendo cose bizzarre.
  • Il libro sembra concentrarsi su domande "trabocchetto". Si può solo sperare che la risposta in fondo sia stata "Ciò che questa espressione fa dipende da strani casi limite in un esempio artificioso per produrre effetti collaterali che nessuno si aspetta. Non farlo mai".

1
L'assegnazione di una delle due variabili è il solito caso dell'operatore ternario, ma ci sono occasioni in cui è utile avere una forma di espressione di if(ad esempio, l'espressione di incremento in un ciclo for). Il contesto più ampio potrebbe benissimo essere for (x = 0, y=0; x+y < 100; someValue?(++x, ++y) :( --x, --y))con un ciclo che può modificare xe yindipendentemente.
Martin Bonner supporta Monica

@MartinBonner Non sono convinto, e questo esempio sembra chiarire abbastanza bene il punto di bo-perrson, come ha citato Tony Hoare.
Taryn
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.