Esiste effettivamente un motivo per il sovraccarico di && e || non corto circuito?


137

Il comportamento in corto circuito degli operatori &&ed ||è uno strumento straordinario per i programmatori.

Ma perché perdono questo comportamento quando sono sovraccarichi? Capisco che gli operatori sono semplicemente zucchero sintattico per le funzioni, ma gli operatori per boolquesto comportamento, perché dovrebbe essere limitato a questo singolo tipo? C'è qualche ragionamento tecnico dietro questo?


1
@PiotrS. Questa domanda è probabilmente la risposta. Immagino che lo standard potrebbe definire una nuova sintassi solo per questo scopo. Probabilmente come operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
iFreilicht,

1
@PiotrS .: Considerare logica tre stati: {true, false, nil}. Dal momento nil&& x == nilche potrebbe corto circuito.
Salterio,

1
@MSalters: considera std::valarray<bool> a, b, c;, come immagini di a || b || cessere in corto circuito?
Piotr Skotnicki,

4
@PiotrS .: Sto sostenendo che esiste almeno un tipo non bool per il fatto che il corto circuito ha senso. Non sto sostenendo che il cortocircuito abbia senso per ogni tipo di non-bool.
Salterio,

3
Nessuno lo ha ancora menzionato, ma c'è anche il problema della retrocompatibilità. A meno che non si presti particolare attenzione a limitare le circostanze in cui si applicherebbe questo cortocircuito, tale cortocircuito potrebbe violare il codice esistente che sovraccarica operator&&o operator||e dipende dalla valutazione di entrambi gli operandi. Il mantenimento della compatibilità con le versioni precedenti è (o dovrebbe essere) importante quando si aggiungono funzionalità a una lingua esistente.
David Hammen,

Risposte:


151

Tutti i processi di progettazione comportano compromessi tra obiettivi reciprocamente incompatibili. Sfortunatamente, il processo di progettazione per l' &&operatore sovraccarico in C ++ ha prodotto un risultato finale confuso: che la stessa caratteristica che si desidera &&- il suo comportamento di corto circuito - è omessa.

I dettagli di come è finito quel processo di progettazione in questo luogo sfortunato, quelli che non conosco. È tuttavia rilevante vedere come un successivo processo di progettazione abbia tenuto conto di questo risultato spiacevole. In C #, l' &&operatore sovraccarico è in corto circuito. Come sono riusciti i progettisti di C #?

Una delle altre risposte suggerisce "sollevamento lambda". Questo è:

A && B

potrebbe essere realizzato come qualcosa moralmente equivalente a:

operator_&& ( A, ()=> B )

dove il secondo argomento utilizza alcuni meccanismi per la valutazione pigra in modo che, quando valutati, vengano prodotti gli effetti collaterali e il valore dell'espressione. L'implementazione dell'operatore sovraccarico farebbe la valutazione pigra solo quando necessario.

Questo non è ciò che ha fatto il team di progettazione di C #. (A parte: se sollevamento lambda è quello che ho fatto quando è arrivato il momento di fare rappresentazione albero espressione del ??operatore, che richiede alcune operazioni di conversione essere effettuato pigramente Descrivendo che in dettaglio sarebbe tuttavia un importante digressione Basti dire:.. Sollevamento lambda funziona ma è sufficientemente pesante da volerlo evitare.)

Piuttosto, la soluzione C # suddivide il problema in due problemi separati:

  • dovremmo valutare l'operando di destra?
  • se la risposta a quanto sopra è stata "sì", come combiniamo i due operandi?

Pertanto, il problema viene risolto rendendo illegale il sovraccarico &&diretto. Piuttosto, in C # devi sovraccaricare due operatori, ognuno dei quali risponde a una di quelle due domande.

class C
{
    // Is this thing "false-ish"? If yes, we can skip computing the right
    // hand size of an &&
    public static bool operator false (C c) { whatever }

    // If we didn't skip the RHS, how do we combine them?
    public static C operator & (C left, C right) { whatever }
    ...

(A parte: in realtà, tre. C # richiede che se falseviene fornito l'operatore, trueanche l' operatore deve essere fornito, il che risponde alla domanda: questa cosa è "vero-vero?". In genere non ci sarebbe motivo di fornire solo uno di questi operatori, quindi C # richiede entrambi.)

Considera una dichiarazione del modulo:

C cresult = cleft && cright;

Il compilatore genera codice per questo come si pensava che tu avessi scritto questo pseudo-C #:

C cresult;
C tempLeft = cleft;
cresult = C.false(tempLeft) ? tempLeft : C.&(tempLeft, cright);

Come puoi vedere, il lato sinistro viene sempre valutato. Se si determina che è "falso-ish", allora è il risultato. Altrimenti, viene valutato il lato destro e viene richiamato l' operatore desideroso definito dall'utente &.

L' ||operatore è definito in modo analogo, come un'invocazione dell'operatore vero e |dell'operatore desideroso :

cresult = C.true(tempLeft) ? tempLeft : C.|(tempLeft , cright);

Con la definizione di tutti e quattro gli operatori - true, false, &e |- C # consente non solo di dire cleft && cright, ma anche non-corto circuito cleft & cright, e anche if (cleft) if (cright) ..., e c ? consequence : alternativee while(c), e così via.

Ora, ho detto che tutti i processi di progettazione sono il risultato di un compromesso. Qui i progettisti del linguaggio C # sono riusciti a ottenere cortocircuiti &&e ||giusti, ma per farlo è necessario sovraccaricare quattro operatori anziché due , cosa che alcune persone trovano confusa. La funzione true / false dell'operatore è una delle funzionalità meno conosciute in C #. L'obiettivo di avere un linguaggio sensibile e diretto, familiare agli utenti di C ++, era contrastato dai desideri di corto circuito e dal desiderio di non implementare il lambda lifting o altre forme di valutazione pigra. Penso che fosse una posizione di compromesso ragionevole, ma è importante rendersi conto che si tratta di una posizione di compromesso. Solo un diverso posizione di compromesso rispetto a quella dei progettisti di C ++.

Se l'argomento del design del linguaggio per tali operatori ti interessa, considera di leggere le mie serie sul perché C # non definisce questi operatori su valori booleani nulla:

http://ericlippert.com/2012/03/26/null-is-not-false-part-one/


1
@Deduplicatore: potresti anche essere interessato a leggere questa domanda e le risposte: stackoverflow.com/questions/5965968/…
Eric Lippert,

5
In questo caso, penso che il compromesso sia più che giustificato. Le cose complicate sono qualcosa di cui solo l'architetto di una biblioteca di classe deve preoccuparsi, e in cambio di questa complicazione, rende il consumo della biblioteca più semplice e intuitivo.
Cody Grey

1
@EricLippert Credo che Envision affermasse di aver visto questo post e di aver pensato che fossi tu ... poi ho visto che aveva ragione. Non stava dicendo che your postè irrilevante. His noticing your distinct writing styleè irrilevante.
WernerCD,

5
Il team di Microsoft non ha abbastanza credito per (1) fare un notevole sforzo per fare la cosa giusta in C # e (2) farlo bene più volte che no.
codenheim,

2
@Voo: se si sceglie di implementare una conversione implicita, boolè possibile utilizzare &&e ||senza implementare operator true/falseo operator &/|in C # nessun problema. Il problema sorge proprio nella situazione in cui non vi è alcuna conversione boolpossibile o dove non si desidera.
Eric Lippert,

43

Il punto è che (entro i limiti di C ++ 98) l'operando di destra verrebbe passato alla funzione di operatore sovraccarico come argomento. In tal modo, sarebbe già stato valutato . Non c'è nulla che il codice operator||()o operator&&()potrebbe o non possa fare che eviti questo.

L'operatore originale è diverso, perché non è una funzione, ma implementato a un livello inferiore della lingua.

Funzionalità linguistiche aggiuntive avrebbero potuto rendere sintatticamente possibile la non valutazione dell'operando della mano destra . Tuttavia, non si sono preoccupati perché ci sono solo alcuni casi selezionati in cui ciò sarebbe semanticamente utile. (Proprio come ? :, che non è disponibile per il sovraccarico.

(Ci sono voluti 16 anni per ottenere lambda nello standard ...)

Per quanto riguarda l'uso semantico, considerare:

objectA && objectB

Questo si riduce a:

template< typename T >
ClassA.operator&&( T const & objectB )

Pensa a cosa ti piacerebbe fare esattamente con l'oggetto B (di tipo sconosciuto) qui, oltre a chiamare un operatore di conversione boole a come lo diresti in parole per la definizione della lingua.

E se si sta chiamando la conversione a bool, ben ...

objectA && obectB

fa la stessa cosa, ora lo fa? Quindi perché sovraccaricare in primo luogo?


7
bene, il tuo errore logico è di ragionare nella lingua attualmente definita sugli effetti di una lingua diversamente definita. ai vecchi tempi un sacco di neofiti erano soliti farlo. "costruttore virtuale". ci è voluta una quantità eccessiva di spiegazione per farli uscire da questo pensiero scatenato. in ogni caso, con il cortocircuito degli operatori integrati ci sono garanzie sulla non valutazione degli argomenti. tale garanzia sarebbe presente anche per i sovraccarichi definiti dall'utente, se fosse stato definito per loro un corto circuito.
Saluti e hth. - Alf,

1
@iFreilicht: ho praticamente detto la stessa cosa di Deduplicator o Piotr, solo con parole diverse. Ho approfondito un po 'il punto nella risposta modificata. In questo modo era molto più conveniente, le estensioni linguistiche necessarie (ad esempio lambda) non esistevano fino a poco tempo fa e il beneficio sarebbe stato comunque trascurabile. Le poche volte in cui le persone responsabili avrebbero "apprezzato" qualcosa che non era già stato fatto dai costruttori di compilatori, nel 1998, si ritorceva contro. (Vedi export.)
DevSolar,

9
@iFreilicht: un booloperatore di conversione per entrambe le classi ha accesso anche a tutte le variabili membro e funziona perfettamente con l'operatore incorporato. Nient'altro che la conversione in bool non ha comunque senso semantico per la valutazione del corto circuito! Cerca di affrontarlo da un punto di vista semantico, non sintattico: cosa vorresti cercare di ottenere, non come lo faresti.
DevSolar,

1
Devo ammettere che non riesco a pensare a uno. L'unico motivo per cui esiste un corto circuito è perché consente di risparmiare tempo per le operazioni su valori booleani e puoi conoscere il risultato di un'espressione prima di valutare tutti gli argomenti. Con altre operazioni AND, non è così, ed è per questo &che &&non sono lo stesso operatore. Grazie per avermi aiutato a realizzarlo.
iFreilicht,

8
@iFreilicht: Piuttosto, lo scopo del corto circuito è perché il calcolo del lato sinistro può stabilire la verità di un presupposto del lato destro . if (x != NULL && x->foo)richiede corto circuito, non per velocità, ma per sicurezza.
Eric Lippert,

26

Una funzionalità deve essere pensata, progettata, implementata, documentata e spedita.

Ora ci abbiamo pensato, vediamo perché potrebbe essere facile ora (e difficile da fare allora). Inoltre, tieni presente che c'è solo una quantità limitata di risorse, quindi aggiungerlo potrebbe aver tritato qualcos'altro (cosa ti piacerebbe rinunciare per questo?).


In teoria, tutti gli operatori potevano consentire un comportamento in corto circuito con una sola caratteristica linguistica aggiuntiva "minore" , a partire dal C ++ 11 (quando furono introdotti i lambda, 32 anni dopo l'inizio della "C con classi" nel 1979, ancora rispettabile 16 dopo c ++ 98):

Il C ++ avrebbe solo bisogno di un modo per annotare un argomento come pigro-valutato - un nascosto-lambda - per evitare la valutazione fino a quando necessario e consentito (pre-condizioni soddisfatte).


Come sarebbe quella caratteristica teorica (ricorda che qualsiasi nuova funzionalità dovrebbe essere ampiamente utilizzabile)?

Un'annotazione lazy, che si applicava a un argomento di funzione, rende la funzione un modello in attesa di un functor e rende il compilatore impacchettare l'espressione in un funzione:

A operator&&(B b, __lazy C c) {return c;}

// And be called like
exp_b && exp_c;
// or
operator&&(exp_b, exp_c);

Sembrerebbe sotto la copertina come:

template<class Func> A operator&&(B b, Func& f) {auto&& c = f(); return c;}
// With `f` restricted to no-argument functors returning a `C`.

// And the call:
operator&&(exp_b, [&]{return exp_c;});

Prendi nota che il lambda rimane nascosto e verrà chiamato al massimo una volta.
Non ci dovrebbe essere alcun peggioramento delle prestazioni a causa di ciò, a parte le ridotte possibilità di eliminazione della sottoespressione comune.


Oltre alla complessità dell'implementazione e alla complessità concettuale (ogni caratteristica aumenta entrambe, a meno che non le elimini sufficientemente per alcune altre caratteristiche), diamo un'occhiata a un'altra considerazione importante: compatibilità con le versioni precedenti.

Sebbene questa funzionalità linguistica non rompa alcun codice, cambierebbe sottilmente qualsiasi API che ne sfrutti, il che significa che qualsiasi uso nelle librerie esistenti sarebbe un cambiamento silenzioso.

A proposito: questa funzionalità, sebbene più facile da usare, è strettamente più forte della soluzione C # di suddivisione &&e ||in due funzioni ciascuna per una definizione separata.


6
@iFreilicht: qualsiasi domanda del modulo "perché la funzione X non esiste?" ha la stessa risposta: per esistere la funzionalità deve essere stata pensata, considerata una buona idea, progettata, specificata, implementata, testata, documentata e spedita all'utente finale. Se una di queste cose non dovesse accadere, nessuna caratteristica. Una di queste cose non è accaduta con la funzionalità proposta; scoprire quale è un problema di ricerca storica; iniziare a parlare con le persone nel comitato di progettazione se ti interessa quale di queste cose non è mai stata fatta.
Eric Lippert,

1
@EricLippert: E, a seconda del motivo, ripetere fino a quando non viene implementato: forse è stato pensato troppo complicato, e nessuno ha pensato di fare una nuova valutazione. Oppure la rivalutazione si è conclusa con motivi di rigetto diversi rispetto a quelli precedenti. (a proposito: aggiunta l'essenza del tuo commento)
Deduplicator,

@Deduplicator Con i modelli di espressione non sono richiesti né la parola chiave pigra né i lambda.
Sumant,

Da un punto di vista storico, nota che il linguaggio Algol 68 originale aveva una coercizione "procedurale" (oltre che deproceduring, il che significa chiamare implicitamente una funzione senza parametri quando il contesto richiede il tipo di risultato anziché il tipo di funzione). Ciò significa che un'espressione di tipo T in una posizione che richiede un valore di tipo "funzione senza parametri che restituisce T" (scritto " proc T" in Algol 68) verrebbe implicitamente trasformata in un corpo di funzione che restituisce l'espressione fornita (lambda implicita). La funzionalità è stata rimossa (diversamente dal deproceduring) nella revisione della lingua del 1973.
Marc van Leeuwen,

... Per C ++ un approccio simile potrebbe essere quello di dichiarare agli operatori piace &&prendere un argomento di tipo "puntatore alla funzione che restituisce T" e una regola di conversione aggiuntiva che consente a un'espressione di argomento di tipo T di essere implicitamente convertita in un'espressione lambda. Si noti che questa non è una normale conversione, poiché deve essere eseguita a livello sintattico: trasformare in fase di esecuzione un valore di tipo T in una funzione non sarebbe di alcuna utilità poiché la valutazione sarebbe già stata effettuata.
Marc van Leeuwen,

13

Con razionalizzazione retrospettiva, principalmente perché

  • per garantire un corto circuito (senza introdurre una nuova sintassi) gli operatori dovrebbero essere limitati a risultatiprimo argomento effettivo convertibile in bool, e

  • il corto circuito può essere facilmente espresso in altri modi, quando necessario.


Ad esempio, se una classe Tha associati &&e ||operatori, quindi l'espressione

auto x = a && b || c;

dove a, be csono espressioni di tipo T, possono essere espresse con corto circuito come

auto&& and_arg = a;
auto&& and_result = (and_arg? and_arg && b : and_arg);
auto x = (and_result? and_result : and_result || c);

o forse più chiaramente come

auto x = [&]() -> T_op_result
{
    auto&& and_arg = a;
    auto&& and_result = (and_arg? and_arg && b : and_arg);
    if( and_result ) { return and_result; } else { return and_result || b; }
}();

L'apparente ridondanza preserva gli effetti collaterali delle invocazioni dell'operatore.


Mentre la riscrittura lambda è più dettagliata, la sua migliore incapsulamento consente di definire tali operatori.

Non sono del tutto sicuro della conformità standard di tutto quanto segue (ancora un po 'di influenza), ma si compila in modo pulito con Visual C ++ 12.0 (2013) e MinGW g ++ 4.8.2:

#include <iostream>
using namespace std;

void say( char const* s ) { cout << s; }

struct S
{
    using Op_result = S;

    bool value;
    auto is_true() const -> bool { say( "!! " ); return value; }

    friend
    auto operator&&( S const a, S const b )
        -> S
    { say( "&& " ); return a.value? b : a; }

    friend
    auto operator||( S const a, S const b )
        -> S
    { say( "|| " ); return a.value? a : b; }

    friend
    auto operator<<( ostream& stream, S const o )
        -> ostream&
    { return stream << o.value; }

};

template< class T >
auto is_true( T const& x ) -> bool { return !!x; }

template<>
auto is_true( S const& x ) -> bool { return x.is_true(); }

#define SHORTED_AND( a, b ) \
[&]() \
{ \
    auto&& and_arg = (a); \
    return (is_true( and_arg )? and_arg && (b) : and_arg); \
}()

#define SHORTED_OR( a, b ) \
[&]() \
{ \
    auto&& or_arg = (a); \
    return (is_true( or_arg )? or_arg : or_arg || (b)); \
}()

auto main()
    -> int
{
    cout << boolalpha;
    for( int a = 0; a <= 1; ++a )
    {
        for( int b = 0; b <= 1; ++b )
        {
            for( int c = 0; c <= 1; ++c )
            {
                S oa{!!a}, ob{!!b}, oc{!!c};
                cout << a << b << c << " -> ";
                auto x = SHORTED_OR( SHORTED_AND( oa, ob ), oc );
                cout << x << endl;
            }
        }
    }
}

Produzione:

000 -> !! !! || falso
001 -> !! !! || vero
010 -> !! !! || falso
011 -> !! !! || vero
100 -> !! && !! || falso
101 -> !! && !! || vero
110 -> !! && !! vero
111 -> !! && !! vero

Qui ogni !!bang-bang mostra una conversione bool, ovvero un controllo del valore dell'argomento.

Poiché un compilatore può facilmente fare lo stesso e ottimizzarlo ulteriormente, si tratta di un'implementazione dimostrata possibile e ogni richiesta di impossibilità deve essere inserita nella stessa categoria delle richieste di impossibilità in generale, vale a dire, generalmente bollocks.


Mi piacciono le tue sostituzioni da corto circuito, specialmente quella ternaria, che è il più vicino possibile.
iFreilicht,

Ti manca il cortocircuito del &&- dovrebbe esserci una linea aggiuntiva come if (!a) { return some_false_ish_T(); }- e al tuo primo proiettile: il cortocircuito riguarda i parametri convertibili in bool, non i risultati.
Arne Mertz,

@ArneMertz: il tuo commento su "Missing" è apparentemente privo di significato. il commento di cosa si tratta, sì, ne sono consapevole. la conversione in boolè necessaria per eseguire il corto circuito.
Saluti e hth. - Alf,

@ Cheersandhth.-Alf il commento sulla scomparsa è stato per la prima revisione della tua risposta in cui hai fatto cortocircuito ||ma non il &&. L'altro commento era rivolto al "dovrebbe essere limitato ai risultati convertibili in bool" nel tuo primo punto elenco - dovrebbe leggere "limitato ai parametri convertibili in bool" imo.
Arne Mertz,

@ArneMertz: OK, per quanto riguarda il controllo delle versioni, scusate se sto modificando lentamente. Restretto, no è il risultato dell'operatore che deve essere limitato, perché deve essere convertito boolper verificare la presenza di cortocircuiti di ulteriori operatori nell'espressione. Ad esempio, il risultato di a && bdeve essere convertito in boolper verificare la presenza di cortocircuiti dell'OR logico in a && b || c.
Saluti e hth. - Alf,

5

tl; dr : non vale la pena, a causa della domanda molto bassa (chi userebbe la funzione?) rispetto ai costi piuttosto elevati (è necessaria una sintassi speciale).

La prima cosa che mi viene in mente è che il sovraccarico dell'operatore è solo un modo elegante per scrivere funzioni, mentre la versione booleana degli operatori ||e &&sono cose da buitlin. Ciò significa che il compilatore ha la libertà di cortocircuitarli, mentre l'espressione x = y && zcon non booleano ye zdeve condurre a una chiamata a una funzione simile X operator&& (Y, Z). Ciò significherebbe che y && zè solo un modo elegante di scrivere, operator&&(y,z)che è solo una chiamata di una funzione stranamente denominata in cui entrambi i parametri devono essere valutati prima di chiamare la funzione (incluso tutto ciò che riterrebbe appropriato un cortocircuito).

Tuttavia, si potrebbe sostenere che dovrebbe essere possibile rendere la traduzione degli &&operatori un po 'più sofisticata, come lo è per l' newoperatore che si traduce in chiamare la funzione operator newseguita da una chiamata del costruttore.

Tecnicamente questo non sarebbe un problema, si dovrebbe definire una sintassi del linguaggio specifica per il presupposto che consente il corto circuito. Tuttavia, l'uso di cortocircuiti sarebbe limitato ai casi in cui Yè possibile farlo X, oppure ci sarebbero state ulteriori informazioni su come eseguire effettivamente il corto circuito (ovvero calcolare il risultato solo dal primo parametro). Il risultato dovrebbe apparire un po 'così:

X operator&&(Y const& y, Z const& z)
{
  if (shortcircuitCondition(y))
    return shortcircuitEvaluation(y);

  <"Syntax for an evaluation-Point for z here">

  return actualImplementation(y,z);
}

Raramente si vuole sovraccaricare operator||e operator&&, poiché raramente c'è un caso in cui la scrittura a && bè effettivamente intuitiva in un contesto non booleano. Le uniche eccezioni che conosco sono i modelli di espressione, ad esempio per i DSL incorporati. E solo una manciata di quei pochi casi trarrebbe beneficio dalla valutazione del corto circuito. I modelli di espressione di solito no, poiché vengono utilizzati per formare alberi di espressioni che vengono valutati in un secondo momento, quindi è sempre necessario avere entrambi i lati dell'espressione.

In breve: né gli autori di compilatori né gli autori di standard sentivano il bisogno di saltare attraverso i cerchi e definire e implementare una sintassi ingombrante aggiuntiva, solo perché uno su un milione potrebbe avere l'idea che sarebbe bello avere un cortocircuito su definito dall'utente operator&&e operator||- solo per arrivare alla conclusione che non è meno sforzo che scrivere la logica per mano.


Il costo è davvero così alto? Il linguaggio di programmazione D consente di dichiarare i parametri lazyche trasformano implicitamente l'espressione data come argomenti in una funzione anonima. Questo dà alla funzione chiamata la scelta di chiamare quell'argomento o no. Quindi, se la lingua ha già lambda, la sintassi aggiuntiva necessaria è molto piccola. "Pseudocodice": X e (A a, pigro B b) {if (cond (a)) {return short (a); } else {actual (a, b ()); }}
BlackJack,

@BlackJack quel parametro pigro potrebbe essere implementato accettando a std::function<B()>, il che comporterebbe un certo sovraccarico. O se sei disposto a incorporarlo, fallo template <class F> X and(A a, F&& f){ ... actual(a,F()) ...}. E magari sovraccaricarlo con il Bparametro "normale" , in modo che il chiamante possa decidere quale versione scegliere. La lazysintassi può essere più conveniente ma ha un certo compromesso prestazionale.
Arne Mertz,

1
Uno dei problemi con std::functionversus lazyè che il primo può essere valutato più volte. Un parametro pigro fooche viene utilizzato come foo+fooviene ancora valutato solo una volta.
Salterio,

"l'uso dei cortocircuiti sarebbe limitato ai casi in cui Y è convettibile a X" ... no, è limitato ai casi in cui Xpuò essere calcolato basandosi Ysolo. Molto diverso. std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}. A meno che tu non stia utilizzando un uso molto casuale di "conversione".
Mooing Duck,

1
@Sumant possono. Ma puoi anche scrivere a mano la logica di un'usanza di corto circuito operator&&. La domanda non è se sia possibile, ma perché non esiste un modo breve e conveniente.
Arne Mertz,

5

Lambdas non è l'unico modo per introdurre la pigrizia. La valutazione pigra è relativamente semplice usando i modelli di espressione in C ++. Non è necessaria la parola chiave lazye può essere implementata in C ++ 98. Gli alberi delle espressioni sono già menzionati sopra. I modelli di espressione sono alberi di espressione dell'uomo povero (ma intelligente). Il trucco è convertire l'espressione in un albero di istanze ricorsivamente nidificate del Exprmodello. L'albero viene valutato separatamente dopo la costruzione.

I seguenti attrezzi codice cortocircuitati &&e ||gli operatori per la classe Sfintanto che fornisce logical_ande logical_orfunzioni libere ed è convertibile in bool. Il codice è in C ++ 14 ma l'idea è applicabile anche in C ++ 98. Vedi esempio dal vivo .

#include <iostream>

struct S
{
  bool val;

  explicit S(int i) : val(i) {}  
  explicit S(bool b) : val(b) {}

  template <class Expr>
  S (const Expr & expr)
   : val(evaluate(expr).val)
  { }

  template <class Expr>
  S & operator = (const Expr & expr)
  {
    val = evaluate(expr).val;
    return *this;
  }

  explicit operator bool () const 
  {
    return val;
  }
};

S logical_and (const S & lhs, const S & rhs)
{
    std::cout << "&& ";
    return S{lhs.val && rhs.val};
}

S logical_or (const S & lhs, const S & rhs)
{
    std::cout << "|| ";
    return S{lhs.val || rhs.val};
}


const S & evaluate(const S &s) 
{
  return s;
}

template <class Expr>
S evaluate(const Expr & expr) 
{
  return expr.eval();
}

struct And 
{
  template <class LExpr, class RExpr>
  S operator ()(const LExpr & l, const RExpr & r) const
  {
    const S & temp = evaluate(l);
    return temp? logical_and(temp, evaluate(r)) : temp;
  }
};

struct Or 
{
  template <class LExpr, class RExpr>
  S operator ()(const LExpr & l, const RExpr & r) const
  {
    const S & temp = evaluate(l);
    return temp? temp : logical_or(temp, evaluate(r));
  }
};


template <class Op, class LExpr, class RExpr>
struct Expr
{
  Op op;
  const LExpr &lhs;
  const RExpr &rhs;

  Expr(const LExpr& l, const RExpr & r)
   : lhs(l),
     rhs(r)
  {}

  S eval() const 
  {
    return op(lhs, rhs);
  }
};

template <class LExpr>
auto operator && (const LExpr & lhs, const S & rhs)
{
  return Expr<And, LExpr, S> (lhs, rhs);
}

template <class LExpr, class Op, class L, class R>
auto operator && (const LExpr & lhs, const Expr<Op,L,R> & rhs)
{
  return Expr<And, LExpr, Expr<Op,L,R>> (lhs, rhs);
}

template <class LExpr>
auto operator || (const LExpr & lhs, const S & rhs)
{
  return Expr<Or, LExpr, S> (lhs, rhs);
}

template <class LExpr, class Op, class L, class R>
auto operator || (const LExpr & lhs, const Expr<Op,L,R> & rhs)
{
  return Expr<Or, LExpr, Expr<Op,L,R>> (lhs, rhs);
}

std::ostream & operator << (std::ostream & o, const S & s)
{
  o << s.val;
  return o;
}

S and_result(S s1, S s2, S s3)
{
  return s1 && s2 && s3;
}

S or_result(S s1, S s2, S s3)
{
  return s1 || s2 || s3;
}

int main(void) 
{
  for(int i=0; i<= 1; ++i)
    for(int j=0; j<= 1; ++j)
      for(int k=0; k<= 1; ++k)
        std::cout << and_result(S{i}, S{j}, S{k}) << std::endl;

  for(int i=0; i<= 1; ++i)
    for(int j=0; j<= 1; ++j)
      for(int k=0; k<= 1; ++k)
        std::cout << or_result(S{i}, S{j}, S{k}) << std::endl;

  return 0;
}

5

Il corto circuito degli operatori logici è consentito perché si tratta di una "ottimizzazione" nella valutazione delle tabelle di verità associate. È una funzione della logica stessa e questa logica è definita.

C'è davvero un motivo per cui sovraccarico &&e ||non corto circuito?

Gli operatori logici con sovraccarico personalizzato non sono obbligati a seguire la logica di queste tabelle di verità.

Ma perché perdono questo comportamento quando sono sovraccarichi?

Quindi l'intera funzione deve essere valutata come di consueto. Il compilatore deve considerarlo come un normale operatore (o funzione) sovraccarico e può comunque applicare ottimizzazioni come farebbe con qualsiasi altra funzione.

Le persone sovraccaricano gli operatori logici per una serie di motivi. Per esempio; possono avere un significato specifico in un dominio specifico che non è quello "normale" logico a cui le persone sono abituate.


4

Il corto circuito è dovuto alla tabella di verità di "e" e "o". Come sapresti quale operazione verrà definita dall'utente e come sapresti che non dovrai valutare il secondo operatore?


Come menzionato nei commenti e nella risposta di @Deduplicators, sarebbe possibile con una funzione linguistica aggiuntiva. So che non funziona ora. La mia domanda era quale fosse il ragionamento alla base della mancanza di tale caratteristica.
iFreilicht,

Beh, certamente sarebbe una caratteristica complicata, considerando che dobbiamo avventurarci in un'ipotesi sulla sua definizione da parte dell'utente!
nj-ath,

Che dire : (<condition>)dopo la dichiarazione dell'operatore di specificare una condizione in cui il secondo argomento non viene valutato?
iFreilicht,

@iFreilicht: avresti ancora bisogno di un corpo di funzione unario alternativo.
Salterio,

3

ma gli operatori di bool hanno questo comportamento, perché dovrebbe essere limitato a questo singolo tipo?

Voglio solo rispondere a questa parte. Il motivo è che il built-in &&e le ||espressioni non sono implementati con funzioni come lo sono gli operatori sovraccarichi.

Avere la logica di cortocircuito integrata nella comprensione del compilatore di espressioni specifiche è facile. È proprio come qualsiasi altro flusso di controllo integrato.

Ma il sovraccarico dell'operatore è invece implementato con funzioni che hanno regole particolari, una delle quali è che tutte le espressioni usate come argomenti vengono valutate prima che la funzione venga chiamata. Ovviamente si potrebbero definire regole diverse, ma è un lavoro più grande.


1
Mi chiedo se ogni considerazione è stata data alla questione se sovraccarichi di &&, ||e ,dovrebbe essere consentito? Il fatto che C ++ non abbia un meccanismo che consenta ai sovraccarichi di comportarsi come qualcosa di diverso dalle chiamate di funzione spiega perché i sovraccarichi di tali funzioni non possono fare nient'altro, ma non spiega perché tali operatori siano in primo luogo sovraccaricabili. Sospetto che la vera ragione sia semplicemente che sono stati gettati in un elenco di operatori senza pensarci troppo.
Supercat,
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.