Forum: PC-Programmierung C++ Arithmetische Operatoren vererben / Vermeiden von doppeltem Code


von Alex E. (tecnologic) Benutzerseite


Lesenswert?

Moin Zusammen,

das ist quasi eine Fortführung des Threads 
Beitrag "Zuweisung von std::arrays überladen"
1
class AlphaBeta
2
{
3
  ///< vector as complex number
4
  std::complex<double> m;
5
6
  ///< assignment operator with inverse park transformation
7
  const AlphaBeta& operator=(const DQ &right)
8
  {
9
     /*
10
      * https://de.wikipedia.org/wiki/D/q-Transformation
11
      */
12
     m.real(right.cos() * right.d() - right.sin() * right.q());
13
     m.imag(right.sin() * right.d() + right.cos() * right.q());
14
    return *this;
15
  }
16
17
  constexpr AlphaBeta operator-(){ return AlphaBeta(-m); }
18
19
  friend constexpr AlphaBeta operator+(const AlphaBeta& a,const AlphaBeta& b){ return AlphaBeta(a.m + b.m); }
20
  friend constexpr AlphaBeta operator-(const AlphaBeta& a,const AlphaBeta& b){ return AlphaBeta(a.m - b.m); }
21
  friend constexpr AlphaBeta operator*(const AlphaBeta& a,const AlphaBeta& b){ return AlphaBeta(a.m * b.m); }
22
  friend constexpr AlphaBeta operator/(const AlphaBeta& a,const AlphaBeta& b){ return AlphaBeta(a.m / b.m); }
23
24
  friend constexpr AlphaBeta operator*(const AlphaBeta& a, const double n){ return AlphaBeta(a.m * n); }
25
  friend constexpr AlphaBeta operator/(const AlphaBeta& a, const double n){ return AlphaBeta(a.m / n); }
26
27
  friend constexpr AlphaBeta operator*(const double n, const AlphaBeta& a){ return a * n; }
28
  friend constexpr AlphaBeta operator/(const double n, const AlphaBeta& a){ return AlphaBeta(n / a.m); }
29
30
  const AlphaBeta& operator+=(AlphaBeta &right) { if(this != &right){ m += right.m; } return *this; }
31
  const AlphaBeta& operator-=(AlphaBeta &right) { if(this != &right){ m -= right.m; } return *this; }
32
  const AlphaBeta& operator*=(AlphaBeta &right) { if(this != &right){ m *= right.m; } return *this; }
33
  const AlphaBeta& operator/=(AlphaBeta &right) { if(this != &right){ m /= right.m; } return *this; }
34
35
  const AlphaBeta& operator*=(double &right) { m *= right; return *this; }
36
  const AlphaBeta& operator/=(double &right) { m /= right; return *this; }
37
};
Ich habe die Klasse AlphaBeta definiert die eigentlich nix anderes ist 
als eine complexe Zahl bzw. ein Vektor in einem Orthogonalen 
Koordinatensystem. Jetzt habe ich aber ein weiteres Koordinatensystem 
das zu dem AlphaBeta System gedreht ist. Um im Code schon 
auszuschließen, dass ich Äpfel mit Birnen vergleiche wollte ich den 
std::complex kapseln.

Meine Definition der Klasse DQ sieht also genau so aus, wie die von 
AlphaBeta bis auf 2 static member und andere Zuweisungsoperatoren. Ich 
muss jetzt aber alle Arithmetischen Operatoren die ich für AlphaBeta 
definiert habe auch für DQ definieren.

Was mich jetzt stört ist, das beide Klassen die selben Operatoren können 
müssen und die bis auf die Zuweisung auch noch gleich aussehen! Diesen 
doppelten Code würde ich gern zwecks Wartbarkeit und doppelter Arbeit 
vermeiden. Wie mache ich das am besten?

Mein 1. Lösungsansatz war über eine Basisklasse die alle Operatoren 
implementiert.
1
class Base
2
{
3
  friend constexpr Base operator+(const Base& a,const Base& b){ return Base(a.m + b.m); }
4
  friend constexpr Base operator-(const Base& a,const Base& b){ return Base(a.m - b.m); }
5
  friend constexpr Base operator*(const Base& a,const Base& b){ return Base(a.m * b.m); }
6
  friend constexpr Base operator/(const Base& a,const Base& b){ return Base(a.m / b.m); }
7
};
Jedoch ist dann das Ergebnis von
1
class Alphabeta: public Base ....
2
class DQ: public Base ....
3
4
AlphaBeta foo, bar;
5
6
auto sum = foo + bar; 
7
DQ transformed_sum = foo + bar;
Die Variable sum wird automatisch vom Typ Base sein. Und somit ist jedes 
Zwischenergebnis meiner Koordinaten Berechnungen vom Typ Base. 
Interessant wird das aber bei der nächsten Zeile. foo + bar == Typ Base 
und dann wird ein Base einem Base mit dem Standard operator= zugewiesen. 
Und ich habe den Salat. kotz im kreis Ich habe einen Apfel einer Birne 
zugewiesen.

Ich hoffe aus dem Roman wird mein Problem klar.

Gruß

Tec

von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Tec N. schrieb:
> Moin Zusammen,

Moin, Moin,

...
> Um im Code schon
> auszuschließen, dass ich Äpfel mit Birnen vergleiche wollte ich den
> std::complex kapseln.
>
...
> Was mich jetzt stört ist, das beide Klassen die selben Operatoren können
> müssen und die bis auf die Zuweisung auch noch gleich aussehen! Diesen
> doppelten Code würde ich gern zwecks Wartbarkeit und doppelter Arbeit
> vermeiden. Wie mache ich das am besten?

Mach aus der Klasse ein Template, das einen Typen als Parameter nimmt, 
dann sind zwei Instanzierungen des Templates von einer ander 
unterschiedliche, statische Typen.

> Ich hoffe aus dem Roman wird mein Problem klar.

Sicher braucht es nicht zieg Operationen, um das Problem zu 
verdeutlichen und übersetzbarer, fehlerfreier Code hilft auch.

Ansonsten:
1
AlphaBeta a( 1.0, 1.0 );
2
AlphaBeta b = a;
3
a += a;
4
assert( a != b );

Warum um alles in der Welt ignorierst Du den Fall, dass eine Operation 
zwei mal den selben Operanden hat?

Die "übliche" Form, binäre Operatoren zu implementieren, ist eine freie 
(nicht friend) Funktion auf den die Member-Version aufsetzen zu lassen:
1
class foo
2
{
3
public:
4
    foo& operator+( const foo& rhs );
5
};
6
7
foo operator+( foo lhs, const foo& rhs )
8
{
9
    lhs += rhs;
10
11
    return lhs;
12
}

Gruß,

Torsten

von mh (Gast)


Lesenswert?

Ich fasse mal kurz zusammen, wie ich dein Problem verstehe.
1. Du hast Vektoren in verschiedenen Koordinatensystemen.
2. Du führst unterschiedliche Typen ein, um die Systeme zu trennen.
3. Du führst operator= ein, um Vektoren zuzuweisen.
4. Dieser oprator= ist überladen, damit du Vektoren aus 
unterschiedlichen Systemen zuweisen kannst.
4b. Als Folge davon macht der operator= nicht mehr ausschließlich eine 
Zuweisung.
5. Du hast ein Problem die übrigen Operationen auf den Vektoren sinnvoll 
zu definieren.

Ich würde vermuten, dass die Ursache deiner Probleme Punkt 4 ist. Punkt 
4 widerspricht ganz klar Punkt 2. Zusätzlich ist 4b ein Problem. 
Überladene Operatoren sollten genau das machen was man von ihnen 
erwartet.

Welchen Vorteil erhoffst du dir von dem operator= und impliziten 
Transformationen? Meiner Meinung nach, wird dein Programm dadurch nicht 
lesbarer. Es werden zwei Vektoren aus unterschiedlichen 
Koordinatensystemen addiert a + b. Wenn ich das im Quelltext lese muss 
ich mir die Frage stellen, ob das Ergebnis ein Vektor im System von a 
oder b ist. Dazu muss ich genau wissen welchen Typ a und b haben und wie 
genau der operator+ definiert ist. Würdest du
etwas schreiben wie a + b.transform_to(system_of(a)) ist sofort klar was 
passiert und in welchem System der Ergebnisvektor lebt.

Wenn du deine Operatoren nicht trivial überlädst bekommst du noch andere 
Probleme. Du musst sicherstellen, dass das System der Operatoren 
vollständig und logisch ist und das z.B. Assoziativgesetz, 
Distributivgesetz und Kommutativgesetz gelten. Spielen deine impliziten 
Transformationen fair mit deinen ctors? Denk dran, bei auto e = a + b 
wird der copy-ctor aufgerufen und nicht operator=.

von Alex E. (tecnologic) Benutzerseite


Lesenswert?

Torsten R. schrieb:
> Sicher braucht es nicht zieg Operationen, um das Problem zu
> verdeutlichen und übersetzbarer, fehlerfreier Code hilft auch.

http://cpp.sh/9mia7 hier habe ich mal ein minimal Beispiel 
implementiert.

was mich daran stört ist das ich mit
1
 const dq& operator+=(const dq& right) { if(this != &right){ m += right.m; } return *this; }
2
    const ab& operator+=(const ab& right) { if(this != &right){ m += right.m; } return *this; }
in ab und dq den operator implementieren muss. Denn beide sind faktisch 
gleich.

Torsten R. schrieb:
> Die "übliche" Form, binäre Operatoren zu implementieren, ist eine freie
> (nicht friend) Funktion auf den die Member-Version aufsetzen zu lassen:

Guter Hinweis. Danke.

Torsten R. schrieb:
> Warum um alles in der Welt ignorierst Du den Fall, dass eine Operation
> zwei mal den selben Operanden hat?
1
 friend constexpr AlphaBeta operator+(const AlphaBeta& a,const AlphaBeta& b){ return AlphaBeta(a.m + b.m); }
hat doch 2 Operanden des selben Typs.

@EDIT: auch du meinst
1
 a2 = a * a;
 ? Gibts dafür eine gesonderte Signatur des Operators?

Torsten R. schrieb:
> Mach aus der Klasse ein Template, das einen Typen als Parameter nimmt,
> dann sind zwei Instanzierungen des Templates von einer ander
> unterschiedliche, statische Typen.

Aber wie mache ich das genau ich will eigentlich nur 2 spezielle Typen 
für std::complex<double> haben. Und diese beiden Typen sollen nicht 
einfach zuweisbar sein sondern nur mit einer Transformation. Da der 
Basistyp immer der gleiche ist weiß ich nicht wie ich das Template 
instanzieren soll.
1
using dq = std::complex<double>;
2
using ab = std::complex<double>;
3
4
template<>
5
const dq& dq::operator=(const ab& rhs) { ... }
6
7
template<>
8
const ab& ab::operator=(const dq& rhs) { ... }
Der Code funktioniert doch so nicht weil dq und ab den gleich Typ haben. 
Oder sollte ich den double in zwei Klassen wrappen? Klingt irgendwie 
nach Murks.

Gruß

Tec

: Bearbeitet durch User
von Torsten R. (Firma: Torrox.de) (torstenrobitzki)


Lesenswert?

Tec N. schrieb:
> http://cpp.sh/9mia7 hier habe ich mal ein minimal Beispiel
> implementiert.

Was auffällt, ist das Du unterschiedliche Typen haben möchtest, die aber 
scheinbar ohne weiteres zuweisbar sein sollen. Das wiederspricht sich, 
denn dann:
1
    dq a;
2
    ab b( a ); // läßt sich nicht übersetzen
3
    ab c;
4
    c = a; // läßt sich übersetzen.

> was mich daran stört ist das ich mit
>
1
>  const dq& operator+=(const dq& right) { if(this != &right){ m += 
2
> right.m; } return *this; }
3
>     const ab& operator+=(const ab& right) { if(this != &right){ m += 
4
> right.m; } return *this; }
5
>
> in ab und dq den operator implementieren muss. Denn beide sind faktisch
> gleich.
1
template < class Tag >
2
class foo
3
{
4
public:
5
    foo& operator+=( const foo& rhs )
6
    {
7
        m += rhs.m;
8
    }
9
10
    template < class OtherTag >
11
    foo& operator=( const foo< OtherTag >& );
12
};
13
14
struct dq_tag;
15
struct ab_tag;
16
17
using dq = foo< dq_tag >;
18
using ab = foo< ab_tag >;

Sollte mit einer Implementierung für operator+= auskommen und zumindest 
ein Interface für Zuweisung bieten (auch wenn ich das für nicht sinnvoll 
halte).

> Torsten R. schrieb:
>> Warum um alles in der Welt ignorierst Du den Fall, dass eine Operation
>> zwei mal den selben Operanden hat?

> hat doch 2 Operanden des selben Typs.

Ich meine diesen Blödsinn:
1
const dq& operator+=(const dq& right) { 
2
    if (this != &right){ 
3
       m += right.m; 
4
    } 
5
   
6
    return *this; 
7
}

(btw: Code wird nicht schneller, wenn man ihn kürzer macht; aber 
meistens schlechter lesbar! ;-) (do it as the ints do!)

> Aber wie mache ich das genau ich will eigentlich nur 2 spezielle Typen
> für std::complex<double> haben. Und diese beiden Typen sollen nicht
> einfach zuweisbar sein sondern nur mit einer Transformation.

Dann würde ich die dafür nötige Funktion auch transformation nennen und 
nicht operator=. (Bzw.: Operatoren in C++ sind nix anderes als 
Funktionen mit sehr speziellen Namen; deswegen überlädt man z.B. auch 
keine Operatoren, sondern deklariert bzw. definiert sie).

mfg Torsten

P.S. In C++ braucht mal eine leere Parameterliste nicht mehr mit void 
deklarieren.

von Alex E. (tecnologic) Benutzerseite


Lesenswert?

Torsten R. schrieb:
> Was auffällt, ist das Du unterschiedliche Typen haben möchtest, die aber
> scheinbar ohne weiteres zuweisbar sein sollen. Das wiederspricht sich,
> denn dann:
>     dq a;
>     ab b( a ); // läßt sich nicht übersetzen
>     ab c;
>     c = a; // läßt sich übersetzen.

Ich könnte den Konstruktor natürlich auch implementieren, aber ich 
verstehe worauf du hinaus willst. Der Typ soll sich immer so verhalten 
wie erwartet. Deshalb müsste ich ja so viele quasi gleiche Operatoren 
implementieren, weil ich das selbst immer wieder drüber gefallen bin, 
weshalb ich jetzt hier schreibe ...

Torsten R. schrieb:
> Ich meine diesen Blödsinn:
> const dq& operator+=(const dq& right) {
>     if (this != &right){
>        m += right.m;
>     }
>
>     return *this;
> }
>
> (btw: Code wird nicht schneller, wenn man ihn kürzer macht; aber
> meistens schlechter lesbar! ;-) (do it as the ints do!)
Ach du meinst das if, das habe ich Depp natürlich beim copy und paste 
vergessen. Das ist natürlich nur für den = Operator sinnvoll.

Torsten R. schrieb:
> Dann würde ich die dafür nötige Funktion auch transformation nennen und
> nicht operator=. (Bzw.: Operatoren in C++ sind nix anderes als
> Funktionen mit sehr speziellen Namen; deswegen überlädt man z.B. auch
> keine Operatoren, sondern deklariert bzw. definiert sie).

Das hatte ich bereits so :), ich hatte aber einen Bug bei dem ich einen 
Vektor aus dem einen System auf einen Vektor des anderen Systems addiert 
habe. Deshalb wollte ich das kapseln.

Torsten R. schrieb:
> P.S. In C++ braucht mal eine leere Parameterliste nicht mehr mit void
> deklarieren.

Ist ne C89 Angewohnheit, ich bin bei manchen Systemen gezwungen mit so 
einem antiken Compiler zu arbeiten. Nichts desto trotz will ich mich in 
modernes C++ Einarbeiten.

von mh (Gast)


Lesenswert?

Ich habe eben versucht abzulisten, was alles an den 2 Zeilen falsch ist 
und aufgegeben, da ich einfach nicht genug Zeit habe.
1
dq operator+(dq& a, dq& b){ return dq(a += b); }
2
ab operator+(ab& a, ab& b){ return ab(a += b); }

Hausaufgabe für dich: Kauf die "Effective C++" Bücher von Scott Meyers 
(alle 4) oder etwas äquivalentes und arbeite sie durch.

von Alex E. (tecnologic) Benutzerseite


Lesenswert?

Torsten R. schrieb:
> struct dq_tag;
> struct ab_tag;
>
> using dq = foo< dq_tag >;
> using ab = foo< ab_tag >;
>
> Sollte mit einer Implementierung für operator+= auskommen und zumindest
> ein Interface für Zuweisung bieten (auch wenn ich das für nicht sinnvoll
> halte).

Ich halte das langsam auch nicht mehr für so eine gute Idee. Ich handle 
mir nur andere Probleme ein. Wahrscheinlich ist es besser einfach 2 
structs zu definieren mit einem std::complex als member und damit die 
direkte Zuweisung und Mischung typsicher auszuschließen und wieder die 
Transformationen als extra Funktionen zu implementieren.

von Alex E. (tecnologic) Benutzerseite


Lesenswert?

mh schrieb:
> Hausaufgabe für dich: Kauf die "Effective C++" Bücher von Scott Meyers
> (alle 4) oder etwas äquivalentes und arbeite sie durch.

Die Third Edition habe ich mir schon vorgemerkt. Muss sie nur mal holen.

von mh (Gast)


Lesenswert?

Tec N. schrieb:
> mh schrieb:
>> Hausaufgabe für dich: Kauf die "Effective C++" Bücher von Scott Meyers
>> (alle 4) oder etwas äquivalentes und arbeite sie durch.
>
> Die Third Edition habe ich mir schon vorgemerkt. Muss sie nur mal holen.

Die 3. Edition von Effective C++? Ok das ist Buch 1, danach More 
Effective C++ und dann Effective Modern C++. Und je nachdem wie du dich 
mit der STL auskennst Effective STL.

von Alex E. (tecnologic) Benutzerseite


Lesenswert?

mh schrieb:
> Die 3. Edition von Effective C++? Ok das ist Buch 1, danach More
> Effective C++ und dann Effective Modern C++. Und je nachdem wie du dich
> mit der STL auskennst Effective STL.

Wie es scheint ist mein nächster Lesestoff gesichert.

von genau (Gast)


Lesenswert?

Tec N. schrieb:
> was mich daran stört ist das ich mit const dq& operator+=(const dq&
> right) { if(this != &right){ m += right.m; } return *this; }
>     const ab& operator+=(const ab& right) { if(this != &right){ m +=
> right.m; } return *this; }
> in ab und dq den operator implementieren muss. Denn beide sind faktisch
> gleich.

Darauf würde ich pfeifen. Das bisschen Tipp(oder eher: 
Copy-Paste)-Arbeit führt dazu, dass wenn du dir den Code in ein paar 
Jahren noch einmal anschaust alles schön an seinem Platz finden wirst.
"Wo ist der operator+? Ah, da, alles klar.", "Wie sind denn da die 
Overloads? Ah, ja, sehr schön." im Vergleich zu
1
class X
2
  : public generic_type_traits<X>
3
  , public generic_std_math<typename generic_type_traits<X>::type>
4
5
---(click: goto declaration - wenn's denn funktioniert)--->
6
7
 rebind<typename generic_std_math::operator_std_plus<typename generic_type_traits<X>::result_type> >::other operator+(........................................................................................................)

von Alex E. (tecnologic) Benutzerseite


Lesenswert?

genau schrieb:
> class X
>   : public generic_type_traits<X>
>   , public generic_std_math<typename generic_type_traits<X>::type>
>
> ---(click: goto declaration - wenn's denn funktioniert)--->
>
>  rebind<typename generic_std_math::operator_std_plus<typename
> generic_type_traits<X>::result_type> >::other
> operator+(.............................................................. 
..........................................)

Ok, da sitze in 2 Wochen schon wieder vor und Frage mich was das soll. 
Ich mache das ganze jetzt über verschiedene structs mit einem member. 
Dafür gibt es dann die Transformationen als Funktion. Direktes Zuweisen 
ist damit verboten und in der Signatur der Transformation ist genau drin 
von welchem System in ein anderes transformiert wird.

Gruß

Tec

von genau (Gast)


Lesenswert?

Das klingt äußerst vernünftig.
Wenn man sich die Deklaration einer Klasse anschaut, sollte unmittelbar 
klar sein, welche Opera- und Konstruktoren es gibt. Das war der 
eigentliche Punkt: Man kann natürlich die operatoren +,- usw. in eine 
Basis-Klasse (generic_std_math) hochziehen und dann generell via +=, -= 
usw. definieren. Das führt aber auf längere Sicht zu Problemen. Kaum 
definiert man dann einen operator* mit einem Skalar, um z.B. den Betrag 
von komplexen Zahlen zu skalieren, hat man die wesentlichen Operatoren 
verstreut über mehrere Klassen. Dann kommt vielleicht noch ein anderer 
operator+= mit einem ganz anderen Typen dazu, sagen wir spaßeshalber das 
Resultat einer Addition einer semikomplexen mit beta-z-komplexen Zahl, 
was natürlich ein alpha-3-Triernion ergibt. Schon muss man die 
Basisklasse mit den Operatoren noch einmal weiter templateizen und von 
beiden ableiten. Leichter lesbar werden die dadurch nicht...
Da es eine Eigenschaft des Begriffs der semikomplexen Zahlen ist, was 
eine Addition mit einer anderen Art Zahl ergibt, muss das dann natürlich 
in ein getrenntes Konzept/Klasse wandern, auf der die 
Operatoren-Definitions-Klasse arbeiten kann (generic_type_traits).
Das wäre dann eine künstliche Zerstückelung oder auch "schlechte 
Analyse" des Zahlbegriffs, da man komplexe Zahlen eher nicht über ihre 
Addierbarkeit definiert, sondern Addierbarkeit Teil ihrer Definition 
ist. Ein Mathematiker würde auf die Frage nach dem Wesen addierbarer 
Zahlen mit den Achseln zucken. Jedoch wäre es genau das, was man mit der 
Vererbung von einer die Operatoren definierenden Klasse aussagen würde: 
"Komplexe Zahlen sind ein Spezialfall der addierbaren Zahlen." - So ein 
Nonsense...

Entschuldigung - ich habe heute schon zu lange diskutiert.

von lalala (Gast)


Lesenswert?


Beitrag #5137744 wurde von einem Moderator gelöscht.
Beitrag #5137785 wurde von einem Moderator gelöscht.
Beitrag #5137834 wurde von einem Moderator gelöscht.
Beitrag #5137920 wurde von einem Moderator gelöscht.
Beitrag #5137941 wurde von einem Moderator gelöscht.
Beitrag #5137943 wurde von einem Moderator gelöscht.
Beitrag #5138070 wurde von einem Moderator gelöscht.
Beitrag #5138356 wurde von einem Moderator gelöscht.
Beitrag #5138526 wurde von einem Moderator gelöscht.
Beitrag #5139162 wurde von einem Moderator gelöscht.
Beitrag #5139988 wurde von einem Moderator gelöscht.
Beitrag #5140098 wurde von einem Moderator gelöscht.
Beitrag #5140248 wurde von einem Moderator gelöscht.
Beitrag #5140387 wurde von einem Moderator gelöscht.
Beitrag #5140396 wurde von einem Moderator gelöscht.
Beitrag #5142601 wurde von einem Moderator gelöscht.
Beitrag #5143774 wurde von einem Moderator gelöscht.
Beitrag #5144968 wurde von einem Moderator gelöscht.
Beitrag #5144999 wurde von einem Moderator gelöscht.
Beitrag #5145237 wurde von einem Moderator gelöscht.
Beitrag #5145968 wurde von einem Moderator gelöscht.
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.