Forum: PC-Programmierung [C++] implizite Operatoren


von Zombie (Gast)


Lesenswert?

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:
1
template <typename T, typename SFINAE = void>
2
struct has_postincrement_operator : std::false_type {};
3
4
template <typename T>
5
struct has_postincrement_operator<T, decltype(std::declval<T>()++, void())> : std::true_type {};
6
7
template <typename T, typename SFINAE = void>
8
struct has_preincrement_operator : std::false_type {};
9
10
template <typename T>
11
struct has_preincrement_operator<T, decltype(++std::declval<T>(), void())> : std::true_type {};
12
13
template <typename T>
14
typename std::enable_if<has_preincrement_operator<T>::value && !has_postincrement_operator<T>::value, T>::type operator++(T &v, int) {
15
  T tmp(v);
16
  ++v;
17
  return tmp;
18
}
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

von Sven B. (scummos)


Lesenswert?

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.

: Bearbeitet durch User
von Yalu X. (yalu) (Moderator)


Lesenswert?

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.

von PittyJ (Gast)


Lesenswert?

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

von Rolf M. (rmagnus)


Lesenswert?

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.

: Bearbeitet durch User
von PittyJ (Gast)


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

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.

von Yalu X. (yalu) (Moderator)


Lesenswert?

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.

: Bearbeitet durch Moderator
von Rolf M. (rmagnus)


Lesenswert?

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.

von Sven B. (scummos)


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

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?

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.