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 bool
haben dieses Verhalten. Warum sollte es auf diesen einzelnen Typ beschränkt werden? Gibt es technische Gründe dafür?
operator&&(const Foo& lhs, const Foo& rhs) : (lhs.bars == 0)
{true, false, nil}
. Danil&& x == nil
könnte es kurzschließen.std::valarray<bool> a, b, c;
, wie stellen Sie sicha || b || c
einen Kurzschluss vor?operator&&
oderoperator||
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.Antworten:
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:
könnte als etwas moralisch Äquivalentes verwirklicht werden als:
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:
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.(Abgesehen davon: tatsächlich drei. C # erfordert, dass, wenn ein Operator
false
bereitgestellt wird,true
auch 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:
Der Compiler generiert Code dafür, als ob Sie dieses Pseudo-C # geschrieben hätten:
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:Durch die Definition alle vier Operatoren -
true
,false
,&
und|
- C # können Sie nicht nur sagen ,cleft && cright
sondern auch nicht-Kurzschlusscleft & cright
, und auchif (cleft) if (cright) ...
, undc ? consequence : alternative
undwhile(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/
quelle
your post
es irrelevant ist.His noticing your distinct writing style
ist irrelevant.bool
, können Sie&&
und||
ohne Implementierungoperator true/false
oderoperator &/|
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.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:
Das läuft darauf hinaus:
Ü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 ...
macht das gleiche, jetzt? Warum also überhaupt überladen?
quelle
export
.)bool
Konvertierungsoperator 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 ?&
und&&
nicht der gleiche Betreiber sind. Danke, dass du mir dabei geholfen hast, das zu realisieren.if (x != NULL && x->foo)
erfordert einen Kurzschluss, nicht aus Gründen der Geschwindigkeit, sondern aus Sicherheitsgründen.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:Es würde unter der Decke aussehen wie:
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.quelle
&&
, 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.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 zubool
, undKurzschlüsse können bei Bedarf leicht auf andere Weise ausgedrückt werden.
Wenn zum Beispiel eine Klasse
T
zugeordnet ist&&
und||
Betreiber, dann ist der Ausdruckwo
a
,b
undc
Ausdrücke vom Typ sindT
, können mit Kurzschlüssen ausgedrückt werdenoder vielleicht deutlicher als
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:
Ausgabe:
Hier zeigt jeder
!!
Bang-Bang eine Konvertierung inbool
, 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.
quelle
&&
- es müsste eine zusätzliche Zeile wieif (!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.bool
ist notwendig, um einen Kurzschluss zu machen.||
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 .bool
um zu prüfen, ob weitere Operatoren im Ausdruck kurzgeschlossen sind. Ebenso muss das Ergebnis vona && b
in konvertiert werden, umbool
zu prüfen, ob das logische ODER in kurzgeschlossen ista && b || c
.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 nichtx = y && z
boolesch isty
undz
zu einem Aufruf einer Funktion wie führen mussX operator&& (Y, Z)
. Dies würde bedeuten, dass diesy && z
nur eine ausgefallene Schreibweiseoperator&&(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 dennew
Operator der Fall ist, der in den Aufruf der Funktionoperator new
gefolgt 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
Y
istX
, 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:Man möchte selten überladen
operator||
undoperator&&
, weil es selten einen Fall gibt, in dem das Schreibena && b
in 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&&
undoperator||
- nur zu haben zu dem Schluss zu kommen, dass es nicht weniger Aufwand ist, als die Logik pro Hand zu schreiben.quelle
lazy
die 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 ()); }}std::function<B()>
, was einen gewissen Overhead verursachen würde. Oder wenn Sie bereit sind, es zu inline machen, machen Sie estemplate <class F> X and(A a, F&& f){ ... actual(a,F()) ...}
. Und überladen Sie es möglicherweise mit dem "normalen"B
Parameter, damit der Anrufer entscheiden kann, welche Version er wählen möchte. Dielazy
Syntax mag bequemer sein, hat aber einen gewissen Leistungskompromiss.std::function
versuslazy
ist, dass das erste mehrfach ausgewertet werden kann. Ein Lazy-Parameter,foo
der wie verwendet wird,foo+foo
wird immer noch nur einmal ausgewertet.X
allein berechnet werden kannY
. 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".operator&&
von Hand aufschreiben . Die Frage ist nicht, ob es möglich ist, sondern warum es keinen kurzen bequemen Weg gibt.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
lazy
und 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 derExpr
Vorlage zu konvertieren . Der Baum wird nach dem Bau separat ausgewertet.Der folgende Code implementiert Kurzschlüsse
&&
und||
Operatoren für Klassen,S
sofern diese freie Funktionen bereitstellenlogical_and
und inlogical_or
konvertierbar sindbool
. Der Code ist in C ++ 14, aber die Idee ist auch in C ++ 98 anwendbar. Siehe Live-Beispiel .quelle
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.
Benutzerdefinierte überladene logische Operatoren sind nicht verpflichtet , der Logik dieser Wahrheitstabellen zu folgen.
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.
quelle
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?
quelle
: (<condition>)
nach der Operatordeklaration, um eine Bedingung anzugeben, bei der das zweite Argument nicht ausgewertet wird?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.
quelle
&&
,||
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.