Gott zum Gruße!
Eine Frage die mich schon länger beschäftigt:
Warum kann der Compiler, wenn man z.B. eine präinkrement operator
definiert, nicht selbständig einen postinkrement operator generieren?
Oder wenn std::declval<A>() == std::declval<B>() definiert ist, er
automatisch den Operator für std::declval<B>() == std::declval<A>()
erstellt?
Oder für std::declvar<A>() != std::declval<B>()?
Warum nimmt der Compiler einem diese Arbeit (und zusätzliche
Fehlerquelle) nicht ab? In 99.99% der Fälle ist die Implementation
dieser zusätzlichen Operatoren doch offensichtlich (und für die
restlichen 0.01% könnte man immer noch explizit die impliziten
Operatoren überschreiben).
Ich habe da mal etwas probiert:
Gemäss godbolt scheint gcc und clang das aber sogar zu schlucken:
https://godbolt.org/z/zwg8tG (Visual C++ 2019 jedoch nicht!) Ob das
wirklich gültiges C++ ist, bin ich mir selber nicht sicher. Aber so in
etwa, stell' ich mir das vor.
Meinungen und Kommentare?
Grüsse
Ich glaube es gibt in Boost so Kram, der aus einem minimalen Satz von
Operatoren die restlichen generiert. Geht schon.
Meines Wissens ist der Grund, aus dem das nicht im Sprachstandard
enthalten ist, Einfachheit (wenn du * hast ist nicht unbedingt klar was
/ tun sollte) und Effizienz (wenn du + hast hast du vielleicht etwas
effizienteres für += als + und dann =).
Persönlich bin ich an dieser Stelle der Meinung, dass die minimale Menge
Code nicht unbedingt erstrebenswert ist. Es ist auch akzeptabel, wenn
dieselbe Zeile zweimal im Code steht, wenn der Code dadurch deutlich
einfacher wird. Ich jedenfalls tausche die Datei, die 8 Operatoren
einzeln in je 1 Zeile implementiert, jeden Tag gegen die mit den 27
Zeilen wirrem Template-Code, die die Operatoren automatisch erzeugen.
Code schreiben ist kein Coolness-Wettbewerb.
Zombie schrieb:> Warum kann der Compiler, wenn man z.B. eine präinkrement operator> definiert, nicht selbständig einen postinkrement operator generieren?
Ausgehend von der Semantik der nichtüberladenen Implementierungen dieser
Operatoren hast du bestimmte Vorstellungen, was diese beiden Operatoren
tun und wie sie zueinander in Beziehung stehen sollten.
> Oder wenn std::declval<A>() == std::declval<B>() definiert ist, er> automatisch den Operator für std::declval<B>() == std::declval<A>()> erstellt?
Hier gehst du davon aus, dass der Gleichheitsoperator symmetrisch sein
sollte.
Tatsächlich könnten diese Operatoren aber auch so definiert werden, dass
++a ein Apfelmännchen berechnet, a++ die Festplatte formatiert und a==b
der Variablen b den Wert von a zuweist. Alle drei Definitionen ist in
C++ völlig legal.
Ob das auch guter Stil ist, ist eine andere Frage. Meiner Meinung nach
sollte auch bei der Operatorüberladung das Prinzip der kleinsten
Überraschung Anwendung finden. Aber nicht jeder ist dieser Meinung:
Ich würde bspw. erwarten, dass im Ausdruck a>>b die beiden Operatoren
unverändert bleiben und ich das Ergebnis der Operation einer dritten
Variable zuweisen kann. Beide Erwartungen werden aber nicht erfüllt,
wenn a als std::istream &a = std::cin definiert ist.
Damit tut Herr Stroustrup bzgl. der Stilfrage implizit eine Auffassung
kund, die der meinigen entgegengesetzt ist. Aber er ist nun mal der
"Erfinder" von C++, und ich bin es nicht ;-)
Eine Konsequenz von Stroustrups Auffassung ist, dass die von dir
vorgeschlagenen Defaultdefinitionen für Operatorüberladungen wenig
sinnvoll wären.
In anderen Programmiersprachen wie bspw. Haskell gibt es diese
Defaultdefinitionen durchaus, allerdings entspricht dort das wilde
Überladen von Operatoren nicht der Sprachphilosophie und wird durch das
Typsystem zudem erschwert.
Beispiel zweier Defaultdefinitionen für Zahlentypen (Num) aus der
Standardbibliothek von Haskell:
1
Vorzeichenwechsel: negate x = 0 - x
2
Subtraktion: x - y = x + negate y
Man muss also, wenn man einen neuen Zahlentyp einführen möchte, nur für
eine der beiden Operationen Code schreiben, die andere wird automatisch
generiert. Es dürfen aber auch beide Operationen explizit implementiert
werden, um bspw. die Effizienz zu steigern.
Braucht man das wirklich?
Erhöht das Übersicht?
In meinen >20 Jahren C++ habe ich ca 5 mal einen Operator definiert. Und
++ war nie mit dabei.
In der (meiner) Realität ist die Operatorüberladung doch sehr selten,
und verwirrt dann nur.
https://de.wikipedia.org/wiki/KISS-Prinzip
Sven B. schrieb:> Einfachheit (wenn du * hast ist nicht unbedingt klar was / tun sollte)
Ich wüsste auch nicht, wie ein Compiler aus * automatisch / erstellen
sollte. Aber aus += automatisch + zu machen, sollte eigentlich immer
möglich sein, sofern der Datentyp kopierbar ist.
> und Effizienz (wenn du + hast hast du vielleicht etwas effizienteres für +=> als + und dann =).
Ich hatte schon beide Varianten, also dass es effizienter war, + selbst
zu definieren und auf dessen Basis dann +=, und umgekehrt, also + auf
der Basis von += zu implementieren. Wobei der Compiler ja trotzdem aus
dem, was man definiert hat, automatisch das jeweils andere erzeugen
könnte. Ich hatte jedenfalls noch nie den Fall, dass + und += komplett
unterschiedlich implementiert sein mussten. Das wäre für mich auch als
Anwender einer solchen Schnittstelle überraschend.
Yalu X. schrieb:> Zombie schrieb:>> Warum kann der Compiler, wenn man z.B. eine präinkrement operator>> definiert, nicht selbständig einen postinkrement operator generieren?>> Ausgehend von der Semantik der nichtüberladenen Implementierungen dieser> Operatoren hast du bestimmte Vorstellungen, was diese beiden Operatoren> tun und wie sie zueinander in Beziehung stehen sollten.
Da ergibt sich dann eben die Frage, ob die Sprache von bestimmten
Beziehungen einzelner Operatoren zueinander ausgehen sollte. Wenn, dann
müssten diese für jedes Paar an Operatoren, die in einer solchen
Beziehung stehen, exakt definiert sein.
>> Oder wenn std::declval<A>() == std::declval<B>() definiert ist, er>> automatisch den Operator für std::declval<B>() == std::declval<A>()>> erstellt?>> Hier gehst du davon aus, dass der Gleichheitsoperator symmetrisch sein> sollte.
"Kommutativ" nennt man es. Mir würde kein sinnvoller Grund einfallen,
warum er das nicht sein sollte. Das gilt aber natürlich nicht generell
für alle Operatoren. Wenn a < b ist, gilt dann immer auch, dass b > a
ist? Manche Standard-Klassen wie std::map gehen davon aus und
funktionieren nicht richtig, wenn das nicht gilt. Und bei Skalaren ist a
b zwar das gleiche wie b a, aber für Matrizen gilt das nicht.
> Ob das auch guter Stil ist, ist eine andere Frage. Meiner Meinung nach> sollte auch bei der Operatorüberladung das Prinzip der kleinsten> Überraschung Anwendung finden.
Das sehe ich auch so. Die Operatoren wurden ja auch nicht einfach
überladbar gemacht, damit man sich nicht so viele Funktionsnamen
ausdenken muss, sondern weil sie einen bestimmten Zweck haben. Damit hat
man aber als Nutzer einer Klasse mit überladenen Operatoren auch
bestimmte Erwartungen an das, was diese Operatoren tun. Wenn sie was
anderes tun als das, ist das schlechtes Interface-Design.
> Aber nicht jeder ist dieser Meinung:>> Ich würde bspw. erwarten, dass im Ausdruck a>>b die beiden Operatoren> unverändert bleiben und ich das Ergebnis der Operation einer dritten> Variable zuweisen kann. Beide Erwartungen werden aber nicht erfüllt,> wenn a als std::istream &a = std::cin definiert ist.
Ich fand das auch immer etwas unglücklich, den Schiebe-Operator zum
streamen zu missbrauchen.
> Eine Konsequenz von Stroustrups Auffassung ist, dass die von dir> vorgeschlagenen Defaultdefinitionen für Operatorüberladungen wenig> sinnvoll wären.
Allerdings wurde trotzdem entschieden, dass der Operator = für alle
Klassen automatisch mit einem Operanden der selben Klasse erstellt wird,
wenn möglich. Und es wurde ursprünglich sogar nicht mal ein Weg
vorgesehen, diesen zu entfernen, wenn man ihn doch nicht haben will.
PittyJ schrieb:> Braucht man das wirklich?> Erhöht das Übersicht?
Wenn man Datentypen definieren will, für die die entsprechenden
Operationen sinnvoll implementierbar sind und das tun, was man von
diesen Operatoren erwartet, ja.
Rolf M. schrieb:> PittyJ schrieb:>> Braucht man das wirklich?>> Erhöht das Übersicht?>> Wenn man Datentypen definieren will, für die die entsprechenden> Operationen sinnvoll implementierbar sind und das tun, was man von> diesen Operatoren erwartet, ja.
Ich habe mehrere Projekte mit teilweise 100 und mehr Klassen.
Bildobjekte, Textstrukturen, Hardware-Schnittstellen, PID-Regler,
Thread-Controller...
Und für alle diese Objekte braucht man weder ++, Multiplikator
Operatoren oder ähnliches.
Alleine für eine Hardware-Timer-Klasse brauchte ich + und
Vergleichsoperatoren. das war's in 10 Jahren.
In meinen Augen ist das immer noch überflüssig. Das Leben geht auch ohne
diese Überladungen. Meistens sogar schlüssiger und übersichtlicher.
PittyJ schrieb:> Alleine für eine Hardware-Timer-Klasse brauchte ich + und> Vergleichsoperatoren. das war's in 10 Jahren.>> In meinen Augen ist das immer noch überflüssig. Das Leben geht auch ohne> diese Überladungen. Meistens sogar schlüssiger und übersichtlicher.
Ich glaube dir ja, dass du das nicht brauchst. Du bist aber nicht das
Maß aller Dinge, sprich: Die Schlussfolgerung, dass es dann gar keiner
braucht, ist falsch.
PittyJ schrieb:> Und für alle diese Objekte braucht man weder ++, Multiplikator> Operatoren oder ähnliches.
Überladene Operatoren braucht man bspw. in Iteratoren, die wiederum
Bestandteil von Container-Klassen sind.
Um mittels einer-range based Loop über die Elemente eines Containers
iterieren zu können, müssen für den Iterator mindestens die Operatoren
!=, Präfix-++ und Präfix-* implementiert sein.
In reinen Anwendungsklassen überlädt man Operatoren eher seltener, da
hast du recht. Aber benutzen tust du sie dennoch, nämlich jedesmal, wenn
du eine Schleife über einen std::vector ö.ä. laufen lässt.
Ich habe mir mal eine kleine 3D-Engine gebastelt. Da habe ich mir
entsprechende Vektor-, Matrix- und Quaternion-Klassen geschrieben. Da
ist Operator-Überladung sehr hilfreich. Und einmal habe ich für einen µC
eine Fixed-Point-Klasse geschrieben. Ohne Operator-Überladung sieht der
Code, der die benutzt, einfach sch***e aus.
Rolf M. schrieb:> Sven B. schrieb:>> Einfachheit (wenn du * hast ist nicht unbedingt klar was / tun sollte)>> Ich wüsste auch nicht, wie ein Compiler aus * automatisch / erstellen> sollte. Aber aus += automatisch + zu machen, sollte eigentlich immer> möglich sein, sofern der Datentyp kopierbar ist.
Das stimmt wohl.
>> Hier gehst du davon aus, dass der Gleichheitsoperator symmetrisch sein>> sollte.>> "Kommutativ" nennt man es.
Das allerings stimmt nicht, eine Relation ~ heißt symmetrisch, wenn a ~
b <=> b ~ a.
Sven B. schrieb:> Das allerings stimmt nicht, eine Relation ~ heißt symmetrisch, wenn a ~> b <=> b ~ a.
Ja, ich hatte an Operationen statt an Relationen gedacht.
Sven B. schrieb:> Aber aus += automatisch + zu machen, sollte eigentlich immer>> möglich sein, sofern der Datentyp kopierbar ist.
Geht aber nicht automatisch. Die "übliche" Implementierung der binären
Ops aus ihren Zuweisungsvarianten nennt sich "kanonische"
Implementierung.
Anders sind die relationalen Ops. Das kann man mit dem
"Spaceship"-Operator <=> für sog. structural-types erreichen.
Wilhelm M. schrieb:> Sven B. schrieb:>> Aber aus += automatisch + zu machen, sollte eigentlich immer>>> möglich sein, sofern der Datentyp kopierbar ist.
Das mit dem Zitieren solltest du noch üben. Der Satz stammt von mir.
> Geht aber nicht automatisch.
Warum nicht?