Gibt es tatsächlich einen Grund, warum && und || überladen sind? nicht kurzschließen?

137

Der Kurzschlussverhalten der Operatoren &&und ||ist ein wunderbares Werkzeug für Programmierer.

Aber warum verlieren sie dieses Verhalten, wenn sie überlastet sind? Ich verstehe, dass Operatoren nur syntaktischer Zucker für Funktionen sind, aber die Operatoren für boolhaben dieses Verhalten. Warum sollte es auf diesen einzelnen Typ beschränkt werden? Gibt es technische Gründe dafür?

iFreilicht
quelle
1
@PiotrS. Diese Frage ist wahrscheinlich die Antwort. Ich denke, der Standard könnte eine neue Syntax nur für diesen Zweck definieren. Wahrscheinlich wie operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
iFreilicht
1
@PiotrS.: Betrachten Sie die Drei-Zustands-Logik : {true, false, nil}. Da nil&& x == nilkönnte es kurzschließen.
MSalters
1
@MSalters: Überlegen Sie std::valarray<bool> a, b, c;, wie stellen Sie sich a || b || ceinen Kurzschluss vor?
Piotr Skotnicki
4
@PiotrS.: Ich argumentiere, dass es mindestens einen Nicht-Bool-Typ gibt, für den ein Kurzschluss sinnvoll ist. Ich behaupte nicht, dass Kurzschlüsse für jeden Nicht-Bool-Typ sinnvoll sind .
MSalters
3
Bisher hat dies noch niemand erwähnt, aber es gibt auch das Problem der Abwärtskompatibilität. Sofern nicht besonders darauf geachtet wird, die Umstände zu begrenzen, unter denen dieser Kurzschluss auftreten würde, könnte ein solcher Kurzschluss vorhandenen Code beschädigen, der überlastet operator&&oder operator||von beiden zu bewertenden Operanden abhängt. Die Aufrechterhaltung der Abwärtskompatibilität ist (oder sollte) wichtig, wenn einer vorhandenen Sprache Funktionen hinzugefügt werden.
David Hammen

Antworten:

151

Alle Entwurfsprozesse führen zu Kompromissen zwischen miteinander inkompatiblen Zielen. Leider hat der Entwurfsprozess für den überladenen &&Operator in C ++ zu einem verwirrenden Endergebnis geführt: Die gewünschte Funktion &&- das Kurzschlussverhalten - entfällt.

Die Details, wie dieser Designprozess an diesem unglücklichen Ort endete, die ich nicht kenne. Es ist jedoch wichtig zu sehen, wie ein späterer Entwurfsprozess dieses unangenehme Ergebnis berücksichtigt hat. In C #, der überladenen &&Operator ist Kurzschluss. Wie haben die Designer von C # das erreicht?

Eine der anderen Antworten schlägt "Lambda-Heben" vor. Das ist:

A && B

könnte als etwas moralisch Äquivalentes verwirklicht werden als:

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

wobei das zweite Argument einen Mechanismus für die verzögerte Bewertung verwendet, so dass bei der Bewertung die Nebenwirkungen und der Wert des Ausdrucks erzeugt werden. Die Implementierung des überlasteten Operators würde die verzögerte Auswertung nur bei Bedarf durchführen.

Dies hat das C # -Design-Team nicht getan. (Abgesehen davon: Lambda-Lifting ist das, was ich getan habe, als es an der Zeit war, eine Ausdrucksbaumdarstellung des ??Operators durchzuführen, bei der bestimmte Konvertierungsvorgänge träge ausgeführt werden müssen. Eine detaillierte Beschreibung wäre jedoch ein großer Exkurs. Es genügt zu sagen: Lambda-Lifting funktioniert aber ist so schwer, dass wir es vermeiden wollten.)

Die C # -Lösung unterteilt das Problem vielmehr in zwei separate Probleme:

  • sollten wir den rechten Operanden bewerten?
  • Wenn die Antwort auf das oben Gesagte "Ja" war, wie kombinieren wir dann die beiden Operanden?

Daher wird das Problem dadurch gelöst, dass eine &&direkte Überlastung illegal ist . Vielmehr müssen Sie in C # zwei Operatoren überladen , von denen jeder eine dieser beiden Fragen beantwortet.

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

(Abgesehen davon: tatsächlich drei. C # erfordert, dass, wenn ein Operator falsebereitgestellt wird, trueauch ein Operator bereitgestellt werden muss, was die Frage beantwortet: Ist dieses Ding "wahr"? Typischerweise gibt es keinen Grund, nur einen solchen Operator bereitzustellen, also C # erfordert beides.)

Betrachten Sie eine Erklärung des Formulars:

C cresult = cleft && cright;

Der Compiler generiert Code dafür, als ob Sie dieses Pseudo-C # geschrieben hätten:

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

Wie Sie sehen können, wird die linke Seite immer ausgewertet. Wenn festgestellt wird, dass es sich um "falsch" handelt, ist dies das Ergebnis. Andernfalls wird die rechte Seite ausgewertet und der eifrige benutzerdefinierte Operator &aufgerufen.

Der ||Operator wird in analoger Weise als Aufruf des Operators true und des eifrigen |Operators definiert:

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

Durch die Definition alle vier Operatoren - true, false, &und |- C # können Sie nicht nur sagen , cleft && crightsondern auch nicht-Kurzschluss cleft & cright, und auch if (cleft) if (cright) ..., und c ? consequence : alternativeund while(c)und so weiter.

Jetzt sagte ich, dass alle Designprozesse das Ergebnis von Kompromissen sind. Hier haben die C # -Sprachendesigner es geschafft, kurzgeschlossen &&und ||richtig zu werden, aber dazu müssen vier statt zwei Operatoren überlastet werden , was manche Leute verwirrend finden. Die True / False-Funktion des Operators ist eine der am wenigsten verstandenen Funktionen in C #. Dem Ziel, eine vernünftige und unkomplizierte Sprache zu haben, die C ++ - Benutzern vertraut ist, wurde der Wunsch nach Kurzschluss und der Wunsch, kein Lambda-Lifting oder andere Formen der faulen Bewertung zu implementieren, entgegengesetzt. Ich denke, das war eine vernünftige Kompromissposition, aber es ist wichtig zu erkennen, dass es sich um eine Kompromissposition handelt. Einfach anders Kompromissposition als die Designer von C ++ gelandet.

Wenn Sie das Thema Sprachdesign für solche Operatoren interessiert, lesen Sie in meiner Reihe nach, warum C # diese Operatoren nicht für nullfähige Boolesche Werte definiert:

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

Eric Lippert
quelle
1
@Deduplicator: Sie könnten auch interessiert sein, diese Frage und Antworten zu lesen: stackoverflow.com/questions/5965968/…
Eric Lippert
5
In diesem Fall halte ich den Kompromiss für mehr als gerechtfertigt. Das komplizierte Zeug ist etwas, mit dem sich nur der Architekt einer Klassenbibliothek befassen muss, und im Gegenzug für diese Komplikation macht es den Verbrauch der Bibliothek einfacher und intuitiver.
Cody Gray
1
@EricLippert Ich glaube, Envision hat gesagt, dass er diesen Beitrag gesehen hat und dachte, dass du es bist ... dann habe ich gesehen, dass er Recht hat. Er sagte nicht, dass your postes irrelevant ist. His noticing your distinct writing styleist irrelevant.
WernerCD
5
Das Microsoft-Team erhält nicht genug Anerkennung dafür, dass (1) sich deutlich bemüht hat, das Richtige in C # zu tun, und (2) es mehrmals richtig gemacht hat.
Codenheim
2
@Voo: Wenn Sie eine implizite Konvertierung in implementieren möchten bool, können Sie &&und ||ohne Implementierung operator true/falseoder operator &/|in C # kein Problem verwenden. Das Problem tritt genau in der Situation auf,bool in der keine Umwandlung in möglich ist oder in der eine nicht erwünscht ist.
Eric Lippert
43

Der Punkt ist, dass (innerhalb der Grenzen von C ++ 98) der rechte Operand als Argument an die überladene Operatorfunktion übergeben wird. Dabei würde es bereits ausgewertet . Es gibt nichts das operator||()oderoperator&&() Code tun oder nicht tun könnte, um dies zu vermeiden.

Der ursprüngliche Operator ist anders, da es sich nicht um eine Funktion handelt, sondern auf einer niedrigeren Sprachebene implementiert ist.

Zusätzliche Sprachfunktionen konnten haben nicht-Auswertung des rechten Operanden syntaktisch gemacht möglich . Sie haben sich jedoch nicht darum gekümmert, da es nur einige wenige Fälle gibt, in denen dies semantisch nützlich wäre. (Genau wie ? :, die überhaupt nicht zum Überladen zur Verfügung steht.

(Sie haben 16 Jahre gebraucht, um Lambdas in den Standard zu bringen ...)

Beachten Sie für die semantische Verwendung:

objectA && objectB

Das läuft darauf hinaus:

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

Überlegen Sie, was genau Sie hier mit objectB (unbekannten Typs) tun möchten, außer einen Konvertierungsoperator aufzurufen bool, und wie Sie dies für die Sprachdefinition in Worte fassen würden.

Und wenn Sie sind Umwandlung in Bool Aufruf, gut ...

objectA && obectB

macht das gleiche, jetzt? Warum also überhaupt überladen?

DevSolar
quelle
7
Nun, Ihr logischer Fehler besteht darin, innerhalb der aktuell definierten Sprache über die Auswirkungen einer anders definierten Sprache nachzudenken. Früher haben viele Neulinge das gemacht. "virtueller Konstruktor". Es bedurfte einer übermäßigen Menge an Erklärungen, um sie aus einem solchen Box-Denken herauszuholen. Bei einem Kurzschluss der eingebauten Operatoren gibt es ohnehin Garantien für die Nichtbewertung von Argumenten. Eine solche Garantie würde auch für benutzerdefinierte Überlastungen bestehen, wenn für diese ein Kurzschluss definiert würde.
Prost und hth. - Alf
1
@iFreilicht: Ich habe im Grunde das Gleiche wie Deduplicator oder Piotr gesagt, nur mit anderen Worten. Ich habe ein wenig auf den Punkt in der bearbeiteten Antwort eingegangen. Auf diese Weise war es viel bequemer, notwendige Spracherweiterungen (z. B. Lambdas) gab es bis vor kurzem nicht, und der Nutzen wäre ohnehin vernachlässigbar gewesen. Die wenigen Male, in denen die Verantwortlichen etwas "gemocht" hätten, was noch nicht von Compiler-Entwicklern im Jahr 1998 getan wurde, schlug es fehl. (Siehe export.)
DevSolar
9
@iFreilicht: Ein boolKonvertierungsoperator für jede Klasse hat ebenfalls Zugriff auf alle Elementvariablen und funktioniert problemlos mit dem integrierten Operator. Alles andere als die Umwandlung in Bool macht für die Kurzschlussbewertung sowieso keinen semantischen Sinn! Versuchen Sie, dies von einem semantischen Standpunkt aus zu betrachten, nicht von einem syntaktischen: Was würden Sie erreichen wollen, nicht wie Sie vorgehen würden ?
DevSolar
1
Ich muss zugeben, dass mir keiner einfällt. Der einzige Grund für einen Kurzschluss besteht darin, dass Zeit für Operationen mit Booleschen Werten gespart wird und Sie das Ergebnis eines Ausdrucks kennen, bevor alle Argumente ausgewertet werden. Bei anderen UND-Operationen ist dies nicht der Fall, und deshalb &und&& nicht der gleiche Betreiber sind. Danke, dass du mir dabei geholfen hast, das zu realisieren.
iFreilicht
8
@iFreilicht: Der Zweck des Kurzschlusses besteht vielmehr darin, dass die Berechnung der linken Seite die Wahrheit einer Vorbedingung der rechten Seite feststellen kann . if (x != NULL && x->foo)erfordert einen Kurzschluss, nicht aus Gründen der Geschwindigkeit, sondern aus Sicherheitsgründen.
Eric Lippert
26

Eine Funktion muss überlegt, entworfen, implementiert, dokumentiert und ausgeliefert werden.

Jetzt haben wir darüber nachgedacht, mal sehen, warum es jetzt einfach ist (und dann schwer zu tun ist). Denken Sie auch daran, dass es nur eine begrenzte Menge an Ressourcen gibt. Wenn Sie also etwas hinzufügen, wird möglicherweise etwas anderes gehackt (worauf möchten Sie verzichten?).


Theoretisch könnten alle Operatoren ein Kurzschlussverhalten mit nur einem "kleinen" zusätzlichen Sprachmerkmal ab C ++ 11 zulassen (als Lambdas eingeführt wurden, 32 Jahre nach dem Start von "C mit Klassen" im Jahr 1979, immer noch ein respektables 16 nach c ++ 98):

C ++ würde nur eine Möglichkeit benötigen, ein Argument als faul ausgewertet - ein verstecktes Lambda - zu kommentieren, um die Auswertung zu vermeiden, bis sie erforderlich und zulässig ist (Voraussetzungen erfüllt).


Wie würde diese theoretische Funktion aussehen (Denken Sie daran, dass neue Funktionen allgemein verwendbar sein sollten)?

Eine Annotation lazy, die auf ein Funktionsargument angewendet wird, macht die Funktion zu einer Vorlage, die einen Funktor erwartet, und lässt den Compiler den Ausdruck in einen Funktor packen:

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

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

Es würde unter der Decke aussehen wie:

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;});

Beachten Sie besonders, dass das Lambda verborgen bleibt und höchstens einmal aufgerufen wird.
Abgesehen davon sollte es keine Leistungsverschlechterung geben , abgesehen von verringerten Chancen für die Eliminierung gemeinsamer Subexpressionen.


Neben der Komplexität der Implementierung und der konzeptionellen Komplexität (jedes Feature erhöht beide, es sei denn, es erleichtert diese Komplexität für einige andere Features ausreichend), betrachten wir eine weitere wichtige Überlegung: die Abwärtskompatibilität.

Während dieser Sprachfunktion keinen Code brechen würde, wäre es auf subtile Weise jeden API Mitnahmen ändert Vorteil davon, was bedeutet , würde jede Verwendung in bestehenden Bibliotheken eine stille Bruch Veränderung sein.

Übrigens: Diese Funktion ist zwar einfacher zu verwenden, aber strikt stärker als die C # -Lösung der Aufteilung &&und ||zur getrennten Definition in zwei Funktionen.

Deduplikator
quelle
6
@iFreilicht: Irgendeine Frage der Form "Warum existiert Feature X nicht?" hat die gleiche Antwort: Um zu existieren, muss die Funktion gedacht, als gute Idee angesehen, entworfen, spezifiziert, implementiert, getestet, dokumentiert und an den Endbenutzer geliefert worden sein. Wenn eines dieser Dinge nicht passiert ist, keine Funktion. Eines dieser Dinge ist mit Ihrer vorgeschlagenen Funktion nicht passiert. herauszufinden, welches ein historisches Forschungsproblem ist; Sprechen Sie mit Leuten im Designkomitee, wenn Sie sich darum kümmern, welches dieser Dinge nie getan wurde.
Eric Lippert
1
@EricLippert: Und je nach Grund wiederholen Sie dies, bis es implementiert ist: Vielleicht wurde es für zu kompliziert gehalten, und niemand dachte daran, eine Neubewertung durchzuführen. Oder die Neubewertung endete mit anderen Gründen als früher. (Übrigens: Der Kern Ihres Kommentars wurde hinzugefügt)
Deduplicator
@Deduplicator Bei Ausdrucksvorlagen sind weder das Lazy-Schlüsselwort noch Lambdas erforderlich.
Sumant
Beachten Sie aus historischer Sicht, dass die ursprüngliche Algol 68-Sprache einen "prozeduralen" Zwang hatte (sowie eine Deprozedurierung, was bedeutet, implizit eine parameterlose Funktion aufzurufen, wenn der Kontext den Ergebnistyp und nicht den Funktionstyp erfordert). Dies bedeutet, dass ein Ausdruck vom Typ T an einer Position, die einen Wert vom Typ "parameterlose Funktion, die T zurückgibt " (in Algol 68 " proc T" geschrieben) erfordert , implizit in einen Funktionskörper umgewandelt wird, der den angegebenen Ausdruck zurückgibt (implizites Lambda). Das Feature wurde (im Gegensatz zum Deproceduring) in der Revision der Sprache von 1973 entfernt.
Marc van Leeuwen
... Für C ++ könnte ein ähnlicher Ansatz darin bestehen, Operatoren zu deklarieren &&, die ein Argument vom Typ "Zeiger auf die Funktion, die T zurückgibt", und eine zusätzliche Konvertierungsregel zu verwenden, mit der ein Argumentausdruck vom Typ T implizit in einen Lambda-Ausdruck konvertiert werden kann. Beachten Sie, dass dies keine gewöhnliche Konvertierung ist, da dies auf syntaktischer Ebene erfolgen muss: Zur Laufzeit einen Wert vom Typ T in eine Funktion umzuwandeln, wäre nutzlos, da die Auswertung bereits durchgeführt worden wäre.
Marc van Leeuwen
13

Mit retrospektiver Rationalisierung, hauptsächlich weil

  • Um einen Kurzschluss (ohne Einführung einer neuen Syntax) zu gewährleisten, müssten die Operatoren auf beschränkt werden Ergebnisseaktuelles erstes Argument konvertierbar zu bool, und

  • Kurzschlüsse können bei Bedarf leicht auf andere Weise ausgedrückt werden.


Wenn zum Beispiel eine Klasse Tzugeordnet ist &&und ||Betreiber, dann ist der Ausdruck

auto x = a && b || c;

wo a, bund cAusdrücke vom Typ sind T, können mit Kurzschlüssen ausgedrückt werden

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

oder vielleicht deutlicher als

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; }
}();

Die offensichtliche Redundanz bewahrt alle Nebenwirkungen der Bedieneraufrufe.


Während das Lambda-Umschreiben ausführlicher ist, können durch seine bessere Kapselung solche Operatoren definiert werden.

Ich bin mir der Standardkonformität aller folgenden Punkte nicht ganz sicher (immer noch ein bisschen Influensa), aber es lässt sich sauber mit Visual C ++ 12.0 (2013) und MinGW g ++ 4.8.2 kompilieren:

#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;
            }
        }
    }
}

Ausgabe:

000 -> !! !! || falsch
001 -> !! !! || wahr
010 -> !! !! || falsch
011 -> !! !! || wahr
100 -> !! && !! || falsch
101 -> !! && !! || wahr
110 -> !! && !! wahr
111 -> !! && !! wahr

Hier zeigt jeder !!Bang-Bang eine Konvertierung in bool, dh eine Argumentwertprüfung.

Da ein Compiler das Gleiche leicht tun und zusätzlich optimieren kann, ist dies eine nachgewiesene mögliche Implementierung, und jeder Anspruch auf Unmöglichkeit muss in dieselbe Kategorie wie Unmöglichkeitsansprüche im Allgemeinen eingestuft werden, nämlich im Allgemeinen Blödsinn.

Prost und hth. - Alf
quelle
Ich mag Ihre Kurzschlussersetzungen, besonders die ternäre, die so nah wie möglich ist.
iFreilicht
Sie vermissen den Kurzschluss der &&- es müsste eine zusätzliche Zeile wie if (!a) { return some_false_ish_T(); }- und zu Ihrer ersten Kugel: Beim Kurzschließen geht es um die Parameter, die in bool konvertierbar sind, nicht um die Ergebnisse.
Arne Mertz
@ArneMertz: Ihr Kommentar zu "Missing" ist anscheinend bedeutungslos. der Kommentar darüber, worum es geht, ja, das ist mir bewusst. Umstellung auf boolist notwendig, um einen Kurzschluss zu machen.
Prost und hth. - Alf
@ Cheersandhth.-Alf der Kommentar über das Fehlen war für die erste Überarbeitung Ihrer Antwort, bei der Sie das kurzgeschlossen haben, ||aber nicht das &&. Der andere Kommentar bezog sich in Ihrem ersten Aufzählungspunkt auf "müsste auf Ergebnisse beschränkt werden, die in bool konvertierbar sind" - er sollte "beschränkt auf Parameter , die in bool konvertierbar sind" imo lauten .
Arne Mertz
@ArneMertz: OK, Neuversionierung, sorry, ich bearbeite langsam. Erneut eingeschränkt, nein, es ist das Operatorergebnis, das eingeschränkt werden muss, da es konvertiert werden muss, boolum zu prüfen, ob weitere Operatoren im Ausdruck kurzgeschlossen sind. Ebenso muss das Ergebnis von a && bin konvertiert werden, um boolzu prüfen, ob das logische ODER in kurzgeschlossen ist a && b || c.
Prost und hth. - Alf
5

tl; dr : Die Mühe lohnt sich nicht, da die Nachfrage sehr gering ist (wer würde die Funktion nutzen?) und die Kosten relativ hoch sind (spezielle Syntax erforderlich).

Das erste, was mir in den Sinn kommt, ist, dass das Überladen von Operatoren nur eine ausgefallene Art ist, Funktionen zu schreiben, während die boolesche Version der Operatoren ||und &&Buitlin-Zeug sind. Das bedeutet, dass der Compiler die Freiheit hat, sie kurzzuschließen, während der Ausdruck nicht x = y && zboolesch ist yund zzu einem Aufruf einer Funktion wie führen muss X operator&& (Y, Z). Dies würde bedeuten, dass dies y && znur eine ausgefallene Schreibweise operator&&(y,z)ist , bei der es sich lediglich um einen Aufruf einer seltsam benannten Funktion handelt, bei der beide Parameter vor dem Aufrufen der Funktion ausgewertet werden müssen (einschließlich aller Elemente, die einen Kurzschluss für angemessen halten).

Man könnte jedoch argumentieren, dass es möglich sein sollte, die Übersetzung von &&Operatoren etwas komplexer zu gestalten, wie dies für den newOperator der Fall ist, der in den Aufruf der Funktion operator newgefolgt von einem Konstruktoraufruf übersetzt wird.

Technisch wäre dies kein Problem, man müsste eine Sprachsyntax definieren, die für die Voraussetzung, die einen Kurzschluss ermöglicht, spezifisch ist. Die Verwendung von Kurzschlüssen wäre jedoch auf Fälle beschränkt, in denen dies möglich Yist X, oder es müssten zusätzliche Informationen darüber vorliegen , wie der Kurzschluss tatsächlich durchgeführt werden soll (dh das Ergebnis wird nur aus dem ersten Parameter berechnet). Das Ergebnis müsste ungefähr so ​​aussehen:

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);
}

Man möchte selten überladen operator||und operator&&, weil es selten einen Fall gibt, in dem das Schreiben a && bin einem nicht-booleschen Kontext tatsächlich intuitiv ist. Die einzigen mir bekannten Ausnahmen sind Ausdrucksvorlagen, z. B. für eingebettete DSLs. Und nur eine Handvoll dieser wenigen Fälle würde von einer Kurzschlussbewertung profitieren. Ausdrucksvorlagen tun dies normalerweise nicht, da sie zur Bildung von Ausdrucksbäumen verwendet werden, die später ausgewertet werden. Daher benötigen Sie immer beide Seiten des Ausdrucks.

Kurz gesagt: Weder Compiler-Autoren noch Standard-Autoren hatten das Bedürfnis, durch die Rahmen zu springen und zusätzliche umständliche Syntax zu definieren und zu implementieren, nur weil einer von einer Million auf die Idee kommen könnte, dass es schön wäre, einen Kurzschluss bei benutzerdefinierten operator&&und operator||- nur zu haben zu dem Schluss zu kommen, dass es nicht weniger Aufwand ist, als die Logik pro Hand zu schreiben.

Arne Mertz
quelle
Sind die Kosten wirklich so hoch? Mit der Programmiersprache D können Parameter deklariert werden, lazydie den als Argumente angegebenen Ausdruck implizit in eine anonyme Funktion umwandeln. Dies gibt der aufgerufenen Funktion die Wahl, dieses Argument aufzurufen oder nicht. Wenn die Sprache bereits Lambdas enthält, ist die zusätzliche Syntax sehr klein. ”Pseudocode”: X und (A a, faul B b) {if (cond (a)) {return short (a); } else {actual (a, b ()); }}
BlackJack
@BlackJack, dass der Lazy-Parameter durch Akzeptieren von a implementiert werden kann std::function<B()>, was einen gewissen Overhead verursachen würde. Oder wenn Sie bereit sind, es zu inline machen, machen Sie es template <class F> X and(A a, F&& f){ ... actual(a,F()) ...}. Und überladen Sie es möglicherweise mit dem "normalen" BParameter, damit der Anrufer entscheiden kann, welche Version er wählen möchte. Die lazySyntax mag bequemer sein, hat aber einen gewissen Leistungskompromiss.
Arne Mertz
1
Eines der Probleme mit std::functionversus lazyist, dass das erste mehrfach ausgewertet werden kann. Ein Lazy-Parameter, fooder wie verwendet wird, foo+foowird immer noch nur einmal ausgewertet.
MSalters
"Die Verwendung von Kurzschlüssen wäre auf Fälle beschränkt, in denen Y zu X konvektierbar ist" ... nein, es ist auf Fälle beschränkt, in denen Xallein berechnet werden kann Y. Sehr verschieden. std::ostream& operator||(char* a, lazy char*b) {if (a) return std::cout<<a;return std::cout<<b;}. Es sei denn, Sie verwenden eine sehr gelegentliche Verwendung von "Konvertierung".
Mooing Duck
1
@Sumant können sie. Sie können die Logik eines Kurzschluss-Brauchs aber auch operator&&von Hand aufschreiben . Die Frage ist nicht, ob es möglich ist, sondern warum es keinen kurzen bequemen Weg gibt.
Arne Mertz
5

Lambdas ist nicht der einzige Weg, um Faulheit einzuführen. Die verzögerte Auswertung ist mit Ausdrucksvorlagen in C ++ relativ einfach . Es ist kein Schlüsselwort erforderlich lazyund es kann in C ++ 98 implementiert werden. Ausdrucksbäume werden bereits oben erwähnt. Ausdrucksvorlagen sind arme (aber kluge) Ausdrucksbäume des Menschen. Der Trick besteht darin, den Ausdruck in einen Baum rekursiv verschachtelter Instanziierungen der ExprVorlage zu konvertieren . Der Baum wird nach dem Bau separat ausgewertet.

Der folgende Code implementiert Kurzschlüsse &&und ||Operatoren für Klassen, Ssofern diese freie Funktionen bereitstellen logical_andund in logical_orkonvertierbar sind bool. Der Code ist in C ++ 14, aber die Idee ist auch in C ++ 98 anwendbar. Siehe Live-Beispiel .

#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;
}
Sumant
quelle
5

Das Kurzschließen der logischen Operatoren ist zulässig, da dies eine "Optimierung" bei der Auswertung der zugehörigen Wahrheitstabellen darstellt. Es ist eine Funktion der Logik selbst, und diese Logik ist definiert.

Gibt es tatsächlich einen Grund, warum überlastet &&und ||nicht kurzgeschlossen?

Benutzerdefinierte überladene logische Operatoren sind nicht verpflichtet , der Logik dieser Wahrheitstabellen zu folgen.

Aber warum verlieren sie dieses Verhalten, wenn sie überlastet sind?

Daher muss die gesamte Funktion wie gewohnt bewertet werden. Der Compiler muss es als normalen überladenen Operator (oder Funktion) behandeln und kann weiterhin Optimierungen anwenden, wie dies bei jeder anderen Funktion der Fall wäre.

Die logischen Operatoren werden aus verschiedenen Gründen überlastet. Beispielsweise; Sie können eine bestimmte Bedeutung in einer bestimmten Domäne haben, die nicht die "normalen" logischen sind, an die Menschen gewöhnt sind.

Niall
quelle
4

Der Kurzschluss ist auf die Wahrheitstabelle von "und" und "oder" zurückzuführen. Woher wissen Sie, welche Operation der Benutzer definieren wird, und woher wissen Sie, dass Sie den zweiten Operator nicht bewerten müssen?

nj-ath
quelle
Wie in den Kommentaren und in der Antwort von @Deduplicators erwähnt, wäre dies mit einer zusätzlichen Sprachfunktion möglich. Ich weiß, dass es jetzt nicht funktioniert. Meine Frage war, was der Grund dafür ist, dass es kein solches Merkmal gibt.
iFreilicht
Nun, es wäre sicherlich eine komplizierte Funktion, wenn man bedenkt, dass wir eine Vermutung über die Definition des Benutzers anstellen müssen!
NJ-Ath
Was ist : (<condition>)nach der Operatordeklaration, um eine Bedingung anzugeben, bei der das zweite Argument nicht ausgewertet wird?
iFreilicht
@iFreilicht: Du brauchst immer noch einen alternativen unären Funktionskörper.
MSalters
3

Aber die Operatoren für bool haben dieses Verhalten. Warum sollte es auf diesen einzelnen Typ beschränkt werden?

Ich möchte nur diesen einen Teil beantworten. Der Grund dafür ist, dass die integrierten Funktionen &&und ||Ausdrücke nicht mit Funktionen implementiert werden, wie dies bei überladenen Operatoren der Fall ist.

Es ist einfach, die Kurzschlusslogik in das Verständnis des Compilers für bestimmte Ausdrücke zu integrieren. Es ist wie bei jedem anderen integrierten Kontrollfluss.

Das Überladen von Operatoren wird jedoch stattdessen mit Funktionen implementiert, die bestimmte Regeln haben. Eine davon ist, dass alle als Argumente verwendeten Ausdrücke ausgewertet werden, bevor die Funktion aufgerufen wird. Natürlich könnten unterschiedliche Regeln definiert werden, aber das ist eine größere Aufgabe.

bames53
quelle
1
Ich frage mich , ob jede Rücksicht auf die Frage gegeben , ob Überlastungen &&, ||und ,sollte erlaubt sein? Die Tatsache, dass C ++ keinen Mechanismus hat, der es Überladungen erlaubt, sich wie etwas anderes als Funktionsaufrufe zu verhalten, erklärt, warum die Überladungen dieser Funktionen nichts anderes bewirken können, erklärt aber nicht, warum diese Operatoren überhaupt überladbar sind. Ich vermute, der wahre Grund ist einfach, dass sie ohne viel Nachdenken in eine Liste von Betreibern geworfen wurden.
Supercat