Forum: PC-Programmierung Überladen von Operatoren bei dynamisch erzeugten Objekten


von Klaus (Gast)


Lesenswert?

Hallo,

ich versuche zur Zeit meine C++ Kenntnisse zu verbessern. In dem 
Beispiel wird der Operator "+" überladen. Wenn ich nun in der main() 
statische Objekte definiere funktioniert alles bestens. Versuche ich die 
Objekte dynamisch zu erzeugen und verwende den "+" Operator bekomme ich 
die Fehlermeldung:

invalid operands of types `Complex*' and `Complex*' to binary 
`operator+'

Es kommt mir so vor als nimmt der Compiler den überladenen Operator gar 
nicht wahr. Wo ist mein Denkfehler?


class Complex {

   // Diese beiden Operatorfunktionen muessen auf die
   // privaten Member "m_re" und "m_im" zugreifen können!
   friend Complex operator+ (const Complex &, const Complex &);
   friend Complex operator- (const Complex &, const Complex &);

   public:
      Complex () {}
      Complex (double re, double im) : m_re(re), m_im(im) {}

   private:
      double m_re;
      double m_im;
   };

   Complex operator+ (const Complex &c1, const Complex &c2)
   {
      Complex result;
      result.m_re = c1.m_re + c2.m_re;
      result.m_im = c1.m_im + c2.m_im;
      return result;
   }

   Complex operator- (const Complex &c1, const Complex &c2)
   {
      Complex result;
      result.m_re = c1.m_re - c2.m_re;
      result.m_im = c1.m_im - c2.m_im;
      return result;
   }

   int main()
   {
      /*Complex Complex1(1,3);               keine Probleme
      Complex Complex2(2,4);                 bei statisch
      Complex Complex3;                      erzeugten Objekten
      Complex3 = Complex1 + Complex2;*/
      Complex *Complex1, *Complex2, *Complex3; // Compilerfehler
      Complex1 = new Complex(1,3);             // bei dynamischer
      Complex2 = new Complex(2,4);             // Erzeugung
      Complex3 = Complex1 + Complex2;

      return 0;
   }

von Daniel (root) (Gast)


Lesenswert?

1
      Complex *Complex1, *Complex2, *Complex3; // Compilerfehler
2
      Complex1 = new Complex(1,3);             // bei dynamischer
3
      Complex2 = new Complex(2,4);             // Erzeugung
4
      Complex3 = Complex1 + Complex2;

natürlich geht das nicht. Denn was new dir zurückgibt, ist die Adresse
an der ein konstruirtes Objekt liegt.

*Complex3 = *Complex1 + *Complex2;

Das müsste gehen.

Optional kann man auch noch so überladen

Complex operator+ (const Complex * c1, const Complex  * c2) {
  Complex tmp;
  return tmp;
}

Hat auch etwas overhead, wie der andere Fall auch.

von Klaus W. (mfgkw)


Lesenswert?

Daniel (root) schrieb:
>
1
>       Complex *Complex1, *Complex2, *Complex3; // Compilerfehler
2
>       Complex1 = new Complex(1,3);             // bei dynamischer
3
>       Complex2 = new Complex(2,4);             // Erzeugung
4
>       Complex3 = Complex1 + Complex2;
5
>
>
> natürlich geht das nicht. Denn was new dir zurückgibt, ist die Adresse
> an der ein konstruirtes Objekt liegt.
>
> *Complex3 = *Complex1 + *Complex2;
>
> Das müsste gehen.

Nein, das wird auch nicht gehen.
Grund: Complex3 zeigt auf nichts gültiges.

So müsste es gehen:
       Complex *Complex1, *Complex2, *Complex3; // Compilerfehler
       Complex1 = new Complex(1,3);             // bei dynamischer
       Complex2 = new Complex(2,4);             // Erzeugung
       Complex3 = new Complex(0,0);
       Complex3 = Complex1 + Complex2;


>
> Optional kann man auch noch so überladen
>
> Complex operator+ (const Complex * c1, const Complex  * c2) {
>   Complex tmp;
>   return tmp;
> }
>
> Hat auch etwas overhead, wie der andere Fall auch.

Das ist in (fast) jedem Fall besser.

Nebenbei: Die Namen Complex1 etc. sind extrem ungünstig, weil sie
suggerieren, sie würden etwas vom Typ Complex repräsentieren.
Tatsächlich sind es Zeiger auf Complex, sollten also etwa
p_Complex1 etc. heißen.

von Klaus W. (mfgkw)


Lesenswert?

und noch eine Anmerkung:
die ursprüngliche auskommentierte Version wird im Kommentar als
"statisch" bezeichnet; das ist falsch.
Statische Variablen sind entweder globale Variablen oder lokale,
wenn sie zusätzlich mit static deklariert sind.
Lokale Variablen ohne static (also die auskommentierten)
werden beim Eintritt in ihren Geltungsbereich (bei der
vorhergehenden öffnenden geschweiften Klammer) automatisch
erzeugt und beim Verlassen des Blocks ebenso automatisch wieder
aufgelöst (weshalb sie auch automatische Variablen heißen).

von Karl H. (kbuchegg)


Lesenswert?

Klaus Wachtler schrieb:

>> Complex operator+ (const Complex * c1, const Complex  * c2) {
>>   Complex tmp;
>>   return tmp;
>> }
>>
>> Hat auch etwas overhead, wie der andere Fall auch.
>
> Das ist in (fast) jedem Fall besser.

Bei so etwas hab ich zwiespältige Gefühle (einfach bei Operator 
Überladung schon zuviel erlebt).

Ich denke bei der Überladung von Operatoren sollte man sich an die 
simple Regel halten: Wenn du in Rom bist, machs wie die Römer

Was meint man damit:
Der Versuch 2 Pointer zu addieren wird normalerweise mit einem Error 
quittiert, weil das von der Sprachdefinition her (zu Recht) verboten 
ist. Für meine eigenen Klassen würde ich daher ebenfalls so eine 
Operation nicht zulassen wollen.
'Machs wie die Römer' bedeutet: denk darüber nach, ob es für int 
dieselbe Operation gibt, bzw. wie verhält sich diese, und modellier 
deine eigenen Operatoren nach diesem Vorbild.

Letztenendes gewinnt man dadurch auch nichts, ausser ein bischen Komfort 
an der Aufrufstelle, an der man sich den Dereferenzier-* spart. Im 
Programm selbst bringt das nichts. Das Programm selbst hat deswegen auch 
nicht weniger Arbeit. Ganz im Gegenteil: Referenzen sind in der 
Sprachdefinition so gemacht, dass sie dem Compiler einige Optimierungen 
ermöglichen, so dass der normale Operator nicht wirklich grossartigen 
Overhed erzeugt.

Wenn man einmal weiterdenkt, dann hat man mit so einem Operator genau 
einen speziellen Fall abgedeckt. Was ist mit Mischformen?

   Complex a;
   Complex* b;

   c = a + b;
   c = b + a;

Für all diese Fälle müsste man eigene Operatoren vorsehen.

Was ist, wenn ich die eigentlichen Objekte in einem Smart-Pointer habe?
Was ist, wenn die Objekte in einem Container stecken und ich einen 
Iterator darauf habe?

Baut man aber zuviele Operatoren, dann läuft man wiederrum Gefahr, dass 
man dem Compiler eine Menge Wege eröffnet, wie er einen an sich 
ungültigen Ausdruck doch noch compilierbar macht.

Ich finde: Operatoren sollten als Argumente auf Objektebene mit 
Referenzen arbeiten. So wie die eingebauten Operatoren auch auf zb int 
oder double arbeiten und nicht auf int* oder double*.

Eine Variation des ganzen, die man häufiger sieht, ist ein 
'Copy-Konstruktor', der als Argument keine const Referenz akzeptiert, 
sondern einen const Pointer. Find ich auch nicht so gut. Aus nahezu den 
gleichen Gründen.

von Peter (Gast)


Lesenswert?

Ich denke so ist es sauberer:
1
class Complex;
2
3
class Complex {
4
5
   public:
6
      Complex () {}
7
      Complex (double re, double im) : m_re(re), m_im(im) {}
8
      Complex operator+ (const Complex &c1);
9
      Complex operator- (const Complex &c1);
10
11
   private:
12
      double m_re;
13
      double m_im;
14
};
15
16
   Complex Complex::operator+ (const Complex &c1)
17
   {
18
      Complex result;
19
      result.m_re = m_re + c1.m_re;
20
      result.m_im = m_im + c1.m_im;
21
      return result;
22
   }
23
24
   Complex Complex::operator- (const Complex &c1)
25
   {
26
      Complex result;
27
      result.m_re = m_re - c1.m_re;
28
      result.m_im = m_im - c1.m_im;
29
      return result;
30
   }
31
32
   int main()
33
   {
34
      /*Complex Complex1(1,3);               keine Probleme
35
      Complex Complex2(2,4);                 bei statisch
36
      Complex Complex3;                      erzeugten Objekten
37
      Complex3 = Complex1 + Complex2;*/
38
      Complex *Complex1, *Complex2, *Complex3; // Compilerfehler
39
      Complex1 = new Complex(1,3);             // bei dynamischer
40
      Complex2 = new Complex(2,4);             // Erzeugung
41
42
      Complex3 = new Complex(*Complex1 + *Complex2); ergebniss als zeiger
43
      Complex x = *Complex1 + *Complex2;             ergebniss als objekt
44
45
      return 0;
46
   }

von Klaus W. (mfgkw)


Lesenswert?

Karl heinz Buchegger schrieb:
> Klaus Wachtler schrieb:
>
>>> Complex operator+ (const Complex * c1, const Complex  * c2) {
>>>   Complex tmp;
>>>   return tmp;
>>> }
>>>
>>> Hat auch etwas overhead, wie der andere Fall auch.
>>
>> Das ist in (fast) jedem Fall besser.
>
> Bei so etwas hab ich zwiespältige Gefühle (einfach bei Operator
> Überladung schon zuviel erlebt).
> ...

Sorry, ich nehme alles zurück und behaupte das Gegenteil - ich hatte
den Text nicht richtig gelesen.

Das Überladen des Operators für const Complex * ist natürlich Quatsch
und nicht mal erlaubt (error: ‘Complex operator+(const Complex*,
const Complex*)’ must have an argument of class or enumerated type),
würde es der Compiler zulassen wäre es immer noch kriminell. da hast
du vollkommen recht.

Ich sehe es genauso, nämlich daß überladene Operatoren nur genau
das machen sollten, was man an der Aufrufstelle erwarten würde.
Eine Addition von Zeigern klammheimlich durch eine Addition
komplexer Zahlen zu überschreiben, wäre vollkommen abstrus.

Was ich meinte, daß es regelmäßig besser wäre, ist das was Peter
dann schrieb: die Operatoren in die Klasse mit aufnehmen (wobei ich
sogar auch die Definition immer in die Klasse schreiben würde, aber
das ist vielleicht Geschmackssache; zumindest inline sollten sie sein).

von Klaus (Gast)


Lesenswert?

Vielen Dank an alle.

Ich hab schon befürchtet, daß irgendwo noch ein "*" oder "&" fehlt :-)

von Peter (Gast)


Lesenswert?

Es ist eh nicht sehr sinnvoll bei C++ noch mit Zeiger rumzuhantieren, 
dann bei C++ gehören auch Exception dazu. Wenn du jezt Object mit new 
anlegst dann musst du dich selber um das Delete kümmern.

man kann es mit hilfe von AutoPtr lösen oder immer sauber einen Catch 
block verwenden wo alle objecte gelöscht werden.

Aber man sollte davon ausgehen das bei jeder Anweisung ein Exception 
erfolgen kann.

var x = new var;
machwas( x );
delete x;

und schon hast du ein Speicherloch wenn in machwas eine Exception 
geworfen wird.

von Daniel (root) (Gast)


Lesenswert?

1
Das Überladen des Operators für const Complex * ist natürlich Quatsch
2
und nicht mal erlaubt (error: ‘Complex operator+(const Complex*,
3
const Complex*)’ must have an argument of class or enumerated type),
4
würde es der Compiler zulassen wäre es immer noch kriminell.

Warum? Welcher Compiler?

von Daniel (root) (Gast)


Lesenswert?

1
#include <iostream>
2
#include <cstdlib>
3
4
template <typename type>
5
struct Num {
6
    Num(type a, type b);
7
    template <typename>
8
    friend Num<type> operator+(const Num<type> &, const Num<type> &);
9
    template <typename>
10
    friend std::ostream operator<<(std::ostream & stream, const Num<type> &);
11
    type n1, n2;
12
    // internal
13
    int id;
14
    static int id_count;
15
};
16
17
template <typename type>
18
int Num<type>::id_count = 0;
19
20
template <typename type>
21
Num<type>::Num(type a = type(0), type b = type(0)) : n1(a), n2(b), id(id_count++) {
22
}
23
24
template <typename type>
25
Num<type> operator+(const Num<type> & a, const Num<type> & b) {
26
    return Num<type>(a.n1+b.n1, a.n2+b.n2);
27
}
28
29
/*
30
template <typename type>
31
Num<type> operator+(const Num<type> * pa, const Num<type> * pb) {
32
    Num<type> tmp;
33
    return tmp;
34
}
35
*/
36
37
template <typename type>
38
std::ostream & operator<<(std::ostream & stream, const Num<type> & self) {
39
    return stream << "id=" << self.id 
40
        << " => (" << self.n1 << ',' << self.n2 << ')' << std::endl;
41
}
42
43
int main() {
44
    Num<double> n1(1,1), n2(2), n3;
45
    n3 = n1 + n2;
46
    std::cout << n1 << n2 << n3;
47
    return EXIT_SUCCESS;
48
}
1
matrix:/pool/c++/overload# g++ -o main main.cpp -Wall
2
matrix:/pool/c++/overload# ./main 
3
id=0 => (1,1)
4
id=1 => (2,0)
5
id=3 => (3,1)

wenn man /**/ weglässt gibt es tatsächlich diesen Fehler.
Ich bin schon länger aus der C++ Programmierung raus, dennoch
soweit ich mich richtig erinnere ging das mit gcc3.*

Würde mich interessieren wieso es verboten sein sollte.

von (prx) A. K. (prx)


Lesenswert?

Weil per Sprachdefinition mindestens einer der Operanden ein 
Klassenobjekt oder eine Referenz darauf sein muss.

Es wäre ausserdem alles andere als stimmig, wenn man die Addition zweier 
Pointer per Overloading definieren könnte, die Subtraktion aber nicht. 
Denn die ist bereits über das C Erbe definiert und somit tabu.

von Klaus W. (mfgkw)


Lesenswert?

Daniel (root) schrieb:
>
1
> Das Überladen des Operators für const Complex * ist natürlich Quatsch
2
> und nicht mal erlaubt (error: ‘Complex operator+(const Complex*,
3
> const Complex*)’ must have an argument of class or enumerated type),
4
> würde es der Compiler zulassen wäre es immer noch kriminell.
5
>
>
> Warum? Welcher Compiler?

Die Meldung kam vom gcc 4.3.2.
Ich kann mir kaum vorstellen, daß ältere Versionen das zulassen,
aber weiß es natürlich nicht.

von Karl H. (kbuchegg)


Lesenswert?

Klaus Wachtler schrieb:

> Was ich meinte, daß es regelmäßig besser wäre, ist das was Peter
> dann schrieb: die Operatoren in die Klasse mit aufnehmen (wobei ich
> sogar auch die Definition immer in die Klasse schreiben würde, aber
> das ist vielleicht Geschmackssache; zumindest inline sollten sie sein).

Vorsicht.
Das kommt auf den Operator an.

Bei den Operatoren für +, i, * und / ist diese Lösung meistens nicht 
besser (von Sonderfällen abgesehen).

+, -, *, / und % werden am besten als freistehende Operatoren gelöst. So 
gesehen hatte das der OP schon richtig.

Was ist das Problem?
Machst du diese Operatoren so wie Peter das vorgeschlagen hat, dann hast 
du möglicherweise (kommt auf die Konstruktoren an) eine Asymetrie in der 
Verwendung.
1
class Complex {
2
3
   public:
4
      Complex () {}
5
      Complex (double re, double im = 0.0) : m_re(re), m_im(im) {}
6
      Complex operator+ (const Complex &c1) { ... }
7
      Complex operator- (const Complex &c1) { ... }
8
9
   private:
10
      double m_re;
11
      double m_im;
12
};
13
14
int main()
15
{
16
17
  Complex a;
18
  Complex b;
19
20
  a = b + 2;   // das compiliert. Aus der 2 wird mit dem Konstruktor der
21
               // den imaginär Teil default auf 0 hat, ein Complex geformt,
22
               // und dann b.op+ aufgerufen
23
24
  a = 2 + b;   // das copmiliert nicht. Es gibt keine Möglichkeit wie
25
               // der Compiler das umformen könnte um die Memberfunktion
26
               // op+ aufrufen zu können. Es existiert ganz einfach kein
27
               // Complex Objekt, für das das gehen könnte.
28
}

Gerade in obigem Beispiel ist aber nicht ersichtlich, wieso b + 2 schon, 
2 + b aber nicht compilierbar sein sollte.
Und einen Konstruktor mit dem man auf einfache Weise einen Complex aus 
einer Zahl erzeugen können soll, wird man wohl so gut wie immer in so 
eine Klasse mit aufnehmen.

Wohingegen
1
class Complex {
2
3
   public:
4
      Complex () {}
5
      Complex (double re, double im = 0.0) : m_re(re), m_im(im) {}
6
7
   private:
8
      double m_re;
9
      double m_im;
10
};
11
12
Complex operator+ (const Complex &c1, const Complex &c2) { ... }
13
Complex operator- (const Complex &c1, const Complex &c2) { ... }
14
15
int main()
16
{
17
18
  Complex a;
19
  Complex b;
20
21
  a = b + 2;
22
  a = 2 + b;
23
}

compiliert in beiden Fällen und wird auch in beiden Fällen richtig 
umgesetzt. Ob man den Zugang zu den Member dann über friend 
Deklarationen oder über (wharscheinlich sowieso vorhandenen) Gettern 
macht, ist Geschmackssache.

In diesem konkreten Fall, spielt es auch nicht so sehr die Rolle, ob man 
die Addition bereits bei der Initialisierung des Rückgabeobjektes macht 
oder nicht, trotzdem sollte man sich das gleich angewöhnen. Bei String 
Objekten zb spielt es eine Rolle: Es ist sinnlos sich zunächst ein 
leeres String Objekt zu erzeugen und dann diesem leeren String Objekt 
einen String zuzuweisen. Da kann man auch gleich das neue String Objekt 
mit dem richtig String erzeugen zu lassen. Ist schon interessant (hab 
ich hier in der Firma auch): Auf der einen Seite wird viel Aufwand 
getrieben um Programme möglichst optimal zu schreiben und die 
einfachsten Dinge um Zeit einzusparen werden regelmäsig vernachlässigt 
:-)
1
class Complex {
2
3
   public:
4
      Complex () {}
5
      Complex (double re, double im = 0.0) : m_re(re), m_im(im) {}
6
7
      double real() const  { return m_re }
8
      double imag() const  { return m_im }
9
10
   private:
11
      double m_re;
12
      double m_im;
13
};
14
15
Complex operator+ (const Complex &c1, const Complex &c2)
16
{
17
  return Complex( c1.real() + c2.real(), c1.imag() + c2.imag() );
18
}
19
Complex operator- (const Complex &c1, const Complex &c2)
20
{
21
  return Complex( c1.real() - c2.real(), c1.imag() - c2.imag() );
22
}
23
24
int main()
25
{
26
  Complex a, b(5);
27
28
  Complex c( b + 2 );
29
  Complex d( 2 + b );

Zusammen mit der ominösen 'Named return value optimization (*)' wird der 
Compiler höchst wahrscheinlich aus diesem Code das Optimum herausholen. 
In den Operatoren ist es wahrscheinlich, dass dort noch nicht mal 
temporäre Zwischenobjekte entstehen.

(*) Das ist die einzige Optimierung die im C++ Standard konkret 
angesprochen wird und auch erlaubt wird.
Konkret geht es darum, dass am Beispiel des op+ von da oben, der 
Compiler das Zwischenobjekt für den Return nicht konstruieren muss, 
sondern das Ergebnis auch gleich im Zielobjekt der Zuweisung 
konstruieren darf.

Anstelle von (ich verwende jetzt eine etwas verkürzte Schreibweise)

   Comp op+( const Comp & a, const Comp & b )

   ....

   Comp c( a + b );

(also: a + b aufrufen, temporäres Objekt benutzen um damit c per Copy 
Construktor zu initialisieren)


darf der Compiler das ganze auch so (intern) implementieren

   Comp op+( const Comp & a, const Comp & b, Comp & result )

   // aus Copm c( a + b )  wird
   Comp c; // wobei c als uninitialisiertes Objekt erzeugt wird
   op+ ( a, b, c );

Diese Zusicherung der Legalität im Standard ist wichtig, weil es hier 
ein konzeptionelles Problem gibt: Konzeptionell wird aus dem Operator 
ein Objekt zurückgegeben mit dem dann das eigentliche Objekt per Copy 
Constructor initialisiert wird. Im unteren Fall wird aber der Copy 
Construktor nie aufgerufen! Das Ergebnis wird direkt in der 
Ergebnisvariablen konstruiert.

Daher ist es auch unklug, wenn ein Copy Construktor mehr macht als ein 
Objekt zu konstruieren. Der Compiler darf Aufrufe zum CCtor 
wegoptimieren, wenn er will!

zb. beim inlinen tauchen solche Fälle regelmässig auf. Der C++ Standard 
erlaubt explizit, den Returnvalue bereits in der Zielvariablen zu 
konstruieren und einen CCtor unter den Tisch fallen zu lassen

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.