Forum: PC-Programmierung Probleme mit Überladung von Operatoren in C++


von Christian F. (funke4ever)


Lesenswert?

Hi,

ich hoffe ihr könnt mir helfen.
Folgendes Problem:
Ich soll den '+'-Operator in einer abgeleiteten Klasse überladen. Der 
Prototyp ist vorgegeben mit:
1
FRACTION3 operator+( const FRACTION3 &a, const FRACTION3 &b );
Dieser steht in meiner header-Datei FRACTION3.h.
Jetzt benötige ich noch die Anweisung welche Berechnungen er genau 
durchführen soll. Dies sollte doch in meiner FRACTION3.cpp-Datei (oder 
einer anderen) stehen, soweit richtig oder?
Die Anweisung lautet bei mir zurzeit wie folgt:
1
FRACTION3 operator+(const FRACTION3 &a, const FRACTION3 &b)
2
{
3
  FRACTION3 lsg;
4
  lsg.nenner = a.nenner + b.nenner;
5
  lsg.zaehl = a.zaehler + b.zaehler;
6
  return lsg;
7
}
Jedoch gibt der Compiler als Fehlermeldung "multiple definitions of..." 
aus. Warum?
Ändere ich die Zeile in:
1
FRACTION3 FRACTION3::operator+(const FRACTION3 &a, const FRACTION3 &b)
2
{
3
...
4
}
bekomme ich die Fehlermeldung  "must take either zero or one argument".
Sämtliche Vergleichsoperatoren konnte ich problemlos überladen, aber 
hier klemmt es momentan bei meiner Ausführung.
Ich hab schon gegoogelt, aber seit einer Stunde ist irgendwie Stillstand 
beim Programmieren.
Mir geht es hauptsächlich erst einmal darum das jetzt richtig 
einzubinden, sollten da noch weitere Fehler bezüglich der Referenz und 
Co. sein kann ich mich später selber darum noch kümmern.
Vielleicht kann mir jemand etwas Licht ins Dunkle bringen.

von Klaus W. (mfgkw)


Lesenswert?

Man kann Operatoren auf zwei Arten überladen:
1. global
2. als Methode in der Klasse des ersten Operanden

Im Fall 1 steht die Operatordefinition außerhalb jeder
Klasse und bekommt alls Operanden (1 oder 2, je nachdem)
als Parameter.

Bei 2. steht der Operator als Methode in der Klasse,
*this ist der erste Operand und ggf. kommt ein zweiter
als Parameter über die Parameterliste.

Ich vermute, daß es in der Richtung hakt, habe aber dein
Problem nicht recht verstanden.
Eine Meldung "multiple definitions of..." kenne ich nicht.
Wenn du nicht die echte Fehlermeldung und einen verwertbaren
(und lesbaren) Quelltext zeigst, kann man nicht viel sagen.

(Die Variante 2. ist übrigens im Zweifel zu bevorzugen.)

von Christian F. (funke4ever)


Lesenswert?

Der Header sieht wie folgt aus:
1
class FRACTION3 : public FRACTION2
2
{
3
      public:
4
             FRACTION3();
5
             FRACTION3(int);//ÜBERLADEN UND VERKETTEN!!!
6
             FRACTION3(int, int);
7
             FRACTION3(double, double);
8
             ~FRACTION3();
9
             int print(ostream *o);
10
             bool operator<(FRACTION3);
11
             bool operator<=(FRACTION3);
12
             bool operator>(FRACTION3);
13
             bool operator>=(FRACTION3);
14
             bool operator==(FRACTION3);
15
             bool operator<(int);
16
             bool operator<=(int);
17
             bool operator>(int);
18
             bool operator>=(int);
19
             bool operator==(int);
20
             //FRACTION3 operator+(FRACTION3);
21
             friend FRACTION3 operator+(const FRACTION3 &, const FRACTION3 &);
22
             //FRACTION3 operator+(int a, const FRACTION3 &b);
23
             //FRACTION3 operator+(const FRACTION3 &a, int b);
24
};
25
26
ostream &operator<<(ostream &os, FRACTION3 &theId);
Die Operatoren funktionieren alle und brauchen nicht beachtet werden.

In meiner FRACTION3.cpp steht folgendes :
1
FRACTION3 FRACTION3::operator+(const FRACTION3 &a, const FRACTION3 &b)
2
{
3
...
4
}
Die Fehlermeldung taucht beim kompillieren der *.cpp auf, und lautet bei 
dieser Konstellation:
"FRACTION3 FRACTION3::operator+(const FRACTION3 &, const FRACTION3 
&)must take either zero or one argument"
"no 'FRACTION3 FRACTION3::operator+(const FRACTION3 &, const FRACTION3 
&)' member function declared  in class 'FRACTION3'"
"[Build Error] [fraction3.o] Error 1"

Die zweite Variante hab ich auch realisiert, siehe header. Diese 
funktioniert auch, aber die Vorgabe ist wie gesagt den Prototyp mit zwei 
Parametern zu verwenden.

von Peter (Gast)


Lesenswert?

warum machst du es bei dem operator + anders als bei den anderen?
1
class FRACTION3 : public FRACTION2
2
{
3
      public:
4
             ...
5
             FRACTION3 operator+(const FRACTION3&);
6
}
so müsste es gehen

von Klaus W. (mfgkw)


Lesenswert?

Du willst also den Operator als globale Funktion definieren
(meine Variante 1.) und nicht als Methode (meine 2)?

Dann ist es eine globale Funktion.
Du schreibst aber:
1
FRACTION3 FRACTION3::operator+(const FRACTION3 &a, const FRACTION3 &b)

durch das FRACTION3:: in FRACTION3::operator+... erzählst du
dem Compiler, daß es eine Methode der Klasse wäre.

So dagegen:
1
FRACTION3 operator+(const FRACTION3 &a, const FRACTION3 &b)
wird es zu einer globalen Funktion.

von Peter (Gast)


Lesenswert?

noch etwas sauberer:

FRACTION3 operator+(const FRACTION3&) const;

von Klaus W. (mfgkw)


Lesenswert?

Christian Funke schrieb:
> Jetzt benötige ich noch die Anweisung welche Berechnungen er genau
> durchführen soll.

Das nennt sich übrigens Definition der Funktion.

Christian Funke schrieb:
> Dies sollte doch in meiner FRACTION3.cpp-Datei (oder
> einer anderen) stehen, soweit richtig oder?

Ja.

von Christian F. (funke4ever)


Lesenswert?

Die zweite Variante ist von der Aufgabenstellung her nicht gewollt.
ABER:
Ich bedanke mich Klaus, nun funktioniert es. War wohl zu fixiert auf 
meine Klasse.

Ich wünsche allen noch einen schöne und erholsame Nacht.

von Karl H. (kbuchegg)


Lesenswert?

Christian Funke schrieb:
> Die zweite Variante ist von der Aufgabenstellung her nicht gewollt.

Das ist auch gut so.
operator+ wird besser als globaler Operator implementiert.

Es ist schliesslich nicht wirklich einzusehen, warum in
1
  FRACTION3 a( 5 ), b;
2
3
  b = a + 10;   // I
4
  b = 10 + a;   // II

Statement I funktioniert und Statement II nicht kompiliert, wie es bei 
einem Member-Operator der Fall wäre. Mit einem globalen Operator geht 
aber beides.

von Klaus W. (mfgkw)


Lesenswert?

Da hast du jetzt aber etwas an den Haaren hergezogen, oder ich
habe dich mißverstanden.

FRACTION3+int ebenso wie int+FRACTION3 haben doch erstmal nicht
damit zu tun, wie man FRACTION3+FRACTION3 implementiert?

Dafür müsste man entweder die passenden Operatoren auch noch
überladen (bei int+FRACTION3 zwangsläufig global), oder passende
Konvertierungen schreiben - dann wiederum wird der Operator
für FRACTION3+FRACTION3 aufgerufen werden, den man global oder
als Methode implementieren kann.

von Karl H. (kbuchegg)


Lesenswert?

Klaus Wachtler schrieb:
> Da hast du jetzt aber etwas an den Haaren hergezogen, oder ich
> habe dich mißverstanden.

Wahrscheinlich

>
> FRACTION3+int ebenso wie int+FRACTION3 haben doch erstmal nicht
> damit zu tun, wie man FRACTION3+FRACTION3 implementiert?

Doch.
Ein globaler operator+ (zusammen mit einem Konstruktor) kann alle 3 
Fälle abdecken. Und zwar OHNE dass du explizit die 10 in ein FRACTION3 
Objekt verwandelst, das kann dann der Compiler von alleine machen. Dazu 
muss der Operator aber global sein, damit er als Kandidat überhaupt in 
Frage kommt, denn bei
   10 + a
würden nur Member-Operatoren von int abgeklappert werden (wenn es die 
geben würde). Die Member Operatoren von a sind uninteressant, da a ja 
höchstens Argument für so einen Operator sein könnte.

Den globalen Operator kann der Compiler aber nehmen, denn 1 Argument 
(das zweite) passt perfekt (*). Er muss nur noch einen Weg finden, wie 
er aus dem int ein FRACTION3 Objekt machen kann. Und da es einen 
Konstruktor gibt, der das kann, benutzt ihn der Compiler auch.
Damit hat er beide Argumente fertig, um den globalen op+ einzusetzen.

Du kannst tatsächlich

   b = 10 + a;

schreiben, und es wird dafür der globale op+ aufgerufen, nachdem der 
Compiler aus den 10 ein temporäres FRACTION3 Objekt gemacht hat.

Probiers aus, wenn du mir nicht traust :-)

(*) Das klingt jetzt nach einer seltsamen Begründung. Ist es auch. 
Tatsächlich stellt sich raus, dass das wie bei anderen Fällen auch ist, 
in denen es mehrere Kandidaten für einen Funktionsaufruf gibt, aus denen 
der Compiler auswählen muss: Derjenige mit den wenigsten Konvertierungen 
gewinnt.

So richtig interessant wird das Ganze dann, wenn man der Klasse dann 
auch noch einen Konvertier Operator verpasst, so dass man aus einem 
FRACTION3 Objekt einen int (oder double) erzeugen kann. Der würde dann 
bei

   int c = a;

zum Einsatz kommen.

Jetzt gibt es dann 2 Möglichkeiten, wie

   b = 10 + a;

compiliert werden könnte.

* Aus 10 ein FRACTION3 Objekt machen und den
    op+( const FRACTION3&, const FRACTION& )
  benutzen. Das Ergebnis dann mit dem FRACTION3::op= an b zuweisen

* Oder aber aus a einen int machen, eine int Addition benutzen, aus
  dem Ergebnis wieder mit einem Konstruktor ein FRACTION3 Objekt
  machen und das mit dem FRACTION3::op= an b zuweisen

Die eigentlichen Additionen sind gleichwertig, in jedem Fall muss 1 
Konvertierung gemacht werden und damit wird der Compiler sich 
beschweren, dass das nicht eindeutig ist.

Fazit: Mit zuvielen Konvertier-Operatoren und Konstruktoren kann man 
sich ganz schnell in die Bredullie bringen. Man eröffnet damit dem 
Compiler zu viele Möglichkeiten. Das ist schlecht, denn
* der Compiler setzt sie schamlos ein, um sich einen Weg zu bauen etwas
  trotzdem zu compilieren, was man so eigentlich gar nicht wollte
* Man immer häufiger in nicht entscheidbare Situationen kommt, weil es
  mehrere gleich gute Wege zum Compilieren gibt.

In wieder anderen Fällen hab ich zb (in meiner Vector3D Klasse) den 
binären op* absichtlich als Member Operator gemacht, damit ich durch die 
Schreibweise eine gewisse Kontrolle darüber behalte, wenn er eingesetzt 
wird. Bei Vektoren gibt es ja 3 'Multiplikationen': 
Skalar-Multiplizieren, Kreuzprodukt und Multiplizeieren mit einem Skalar 
um den Vektor zu skalieren. Das war mir zu gefährlich, wenn sich der 
Compiler da frei aus dem Fundus bedienen kann und mir simple logische 
Fehler compiliert. Dadurch das ich gezwungen bin

   b = a * 3;

zu schreiben und

   b = 3 * a;

ein Fehler ist, hab ich das bischen Kontrolle bekommen, das dann schon 
gereicht hat um nicht ständig auf Tippfehler reinzufallen.

von jungspund (Gast)


Lesenswert?

Nur so nebenbei, Brüche werden anders addiert...

von D. I. (Gast)


Lesenswert?

jungspund schrieb:
> Nur so nebenbei, Brüche werden anders addiert...

In der Tat musste auch schon schmunzeln

von Klaus W. (mfgkw)


Lesenswert?

Karl heinz Buchegger schrieb:
> Du kannst tatsächlich
>
>    b = 10 + a;
>
> schreiben, und es wird dafür der globale op+ aufgerufen, nachdem der
> Compiler aus den 10 ein temporäres FRACTION3 Objekt gemacht hat.
>
> Probiers aus, wenn du mir nicht traust :-)

Doch das traue ich dir zu :-)

Wenn du genau das so haben willst, ist ein globaler Operator wohl
sinnvoller. Daß es solche Fälle gibt, weiß ich und behaupte auch
nicht, daß man ihn nie global definieren dürfte. Nur im Zweifelsfall
ist es besser, ihn lokal zu definieren, wenn nichts anderes dagegen
spricht, weil die globalen Operatoren zwangsläufig auch ihre Nachteile
haben.

Ich bin mir aber nicht sicher, ob man das obige Beispiel überhaupt so
stehen lassen sollte.
Es geht darum, 2 Zahlen unterschiedlichen Typs zu addieren.
Das kann gehen, indem man die 10 in ein FRACTION3 konvertiert und zwei
solchige addiert, oder indem man aus a eine int macht und eine
int-Addition durchführt.
Daß hier nur eines von beiden beabsichtigt sein wird, ist klar, aber
das geht aus dem Ausdruck nicht hervor.
Da halte ich es für deutlich besseren Stil, klar zu sagen, was man
will:
1
     b = FRACTION3(10) + a;
oder:
1
     b = 10 + int(a);
Damit entfallen alle Fehler, die man sich (oder anderen, weniger
begabten Mitstreitern) einbrockt, indem man sich auf eine bestimmte
Suchreihenfolge der Operatoren verlässt.

Schließlich sind derart übersichtliche Fälle nicht unbedingt die
Regel; spätestens mit Ableitungen wird es spannender, oder mit
meinen geliebten templates.


Karl heinz Buchegger schrieb:
> Fazit: Mit zuvielen Konvertier-Operatoren und Konstruktoren kann man
> sich ganz schnell in die Bredullie bringen. Man eröffnet damit dem
> Compiler zu viele Möglichkeiten. Das ist schlecht, denn
> * der Compiler setzt sie schamlos ein, um sich einen Weg zu bauen etwas
>   trotzdem zu compilieren, was man so eigentlich gar nicht wollte
> * Man immer häufiger in nicht entscheidbare Situationen kommt, weil es
>   mehrere gleich gute Wege zum Compilieren gibt.

Genau!

Karl heinz Buchegger schrieb:
> In wieder anderen Fällen hab ich zb (in meiner Vector3D Klasse) den
> binären op* absichtlich als Member Operator gemacht, damit ich durch die
> Schreibweise eine gewisse Kontrolle darüber behalte, wenn er eingesetzt
> wird. Bei Vektoren gibt es ja 3 'Multiplikationen':
> Skalar-Multiplizieren, Kreuzprodukt und Multiplizeieren mit einem Skalar
> um den Vektor zu skalieren. Das war mir zu gefährlich, wenn sich der
> Compiler da frei aus dem Fundus bedienen kann und mir simple logische
> Fehler compiliert. Dadurch das ich gezwungen bin
>
>    b = a * 3;
>
> zu schreiben und
>
>    b = 3 * a;
>
> ein Fehler ist, hab ich das bischen Kontrolle bekommen, das dann schon
> gereicht hat um nicht ständig auf Tippfehler reinzufallen.

Das funktioniert aber dann auch nur, solange du damit alleine
unterwegs bist und dir bewusst bist, daß a*3 und 3*a grundverschiedene
Dinge sind.
Da sehe ich bereits das Ende der Automatismen erreicht, die man mit
C++ und überladenen Operatoren hat, weil die Operatoren auf die
üblichen Typen abzielen und in ihrer Syntax gar nicht und in der
Semantik nur sehr bedingt sinnvoll zu ändern sind.
Es geht bereits los damit, daß vektor*vektor sich nur auf eines von
zwei möglichen Produkten beziehen kann und es einen operator X halt
nicht gibt, und geht mit a*3 ungleich 3*a weiter.
Irgendwelche subtilen Unterschiede sehr ähnlicher Ausdrücke zu nutzen,
artet leicht in Mißbrauch aus.

Solange man weiß, was man tut, kann man es natürlich machen.
Generell empfehlen würde ich aber eher nicht.

von Karl H. (kbuchegg)


Lesenswert?

Klaus Wachtler schrieb:

> Generell empfehlen würde ich aber eher nicht.

Nein, natürlich nicht.
Das war auch eher eine Krücke, auf die sich die Entwicklungsmannschaft 
einigen konnte. Wenn man sich mal dran gewöhnt hat, ist es 'sehr 
natürlich' (wie fast alles im Leben)

> Daß hier nur eines von beiden beabsichtigt sein wird, ist klar, aber
> das geht aus dem Ausdruck nicht hervor.
> Da halte ich es für deutlich besseren Stil, klar zu sagen, was man
> will:     b = FRACTION3(10) + a;
>
>
> oder:     b = 10 + int(a);
>
>
> Damit entfallen alle Fehler, die man sich (oder anderen, weniger
> begabten Mitstreitern) einbrockt, indem man sich auf eine bestimmte
> Suchreihenfolge der Operatoren verlässt.

Das 'Problem' an dieser Stelle ist, dass es für

    b = a + 10;

eine definierte Operation gibt, wenn du den op+ als Member machst.
Genau darum geht es, dass es für einen Programmierer, der die Internals 
nicht kennt, unlogisch und nicht einsichtig ist, warum

   b = a + 10;
kompiliert und
   b = 10 + a;
nicht compiliert!

Das ist die eigentliche Motivation, den op+ als globalen Operator zu 
implementieren.

Man möchte in beiden Fällen identisches Verhalten haben, denn genau das 
ist es, was jeder erwarten wird. Je weniger derartige Überraschungen, 
desto besser.

Edit:
Wenn du das hier erzwingen willst
     b = FRACTION3(10) + a;
dann muss hier gedreht werden
1
   explicit FRACTION3(int);//ÜBERLADEN UND VERKETTEN!!!
Den op+ als Member-Op auszuführen, ist für diese Absicht der falsche Weg 
(bzw. es spielt dann keine Rolle mehr)

von Klaus W. (mfgkw)


Lesenswert?

Karl heinz Buchegger schrieb:
> bzw. es spielt dann keine Rolle mehr

genau darauf wollte ich raus.

von Bartli (Gast)


Lesenswert?

Ja, die lieben Vektoren. Bei denen hab ich irgendwann angefangen 
solsches zeugs zu machen:
1
class vec3;
2
3
vec3 cross(const vec3 & a, const vec3 & b);
4
vec3 dot(const vec3 & a, const vec3 & b);

Und operator * ist für Skalarmultiplikation reserviert. Auch ne Krücke, 
halt meine.

von Christian F. (funke4ever)


Lesenswert?

@jungspund

...das weiss ich auch, ich hab da nur erst einmal blödsinn 
reingeschrieben...

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.