Überladen von Operatoren in Klassen

Moin,

ich möchte gerne in einer Klasse Operatoren überladen. Dazu habe ich mir eine einfache Klasse erstellt, die nur einen x und y Wert hat.
Angefangen habe ich erstmal mit einfachen Operatoren wie +=, ++(Postfix), ++(Prefix) und +.
Dazu erstmal meine Codebestandteile.

Meine Header-Datei

#ifndef _testOp_h
#define _testOp_h

#include "Arduino.h"

class TestOp {
	public:
		TestOp();
		TestOp(uint32_t, uint32_t);
		
		void init(uint32_t, uint32_t);
		
		TestOp operator+=(const TestOp &v);
		TestOp operator++(int);				// Postfix-Operator
		TestOp operator++();					// Prefix-Operator
		//TestOp operator+(TestOp, TestOp);		//??? muss friend sein
		friend TestOp operator+(const TestOp&, const TestOp&);
		
		void ink1(uint8_t*);
		void ink2(uint8_t&);
		
		void do1(void (*)());
		void do2(void (&)());
		
		uint32_t x;
		uint32_t y;

	private:
		
};

#endif

die dazugehörige cpp-Datei

#include "_testOp.h"

/** constructor **********************************************/
TestOp::TestOp() {
	
}

TestOp::TestOp(uint32_t x, uint32_t y) {
	init(x, y);
}
/*************************************************************/


// methoden public ********************************************
void TestOp::init(uint32_t xi, uint32_t yi) {
	x = xi;
	y = yi;
}

TestOp TestOp::operator+=(const TestOp& v) {
	x += v.x;
	y += v.y;
	return *this;
}

TestOp TestOp::operator++(int v) {
	x++;
	y++;
	return *this;
}

TestOp TestOp::operator++() {
	TestOp v = *this;
	++x;
	++y;
	return v;
}

//TestOp TestOp::operator+(const TestOp& v1, const TestOp& v2) {		//??? geht nicht
TestOp operator+(const TestOp& v1, const TestOp& v2) {
	TestOp v;
	v.x = v1.x + v2.x;
	v.y = v1.y + v2.y;
	return v;
}

void TestOp::ink1(uint8_t* v) {
	//*v++;							//??? geht nicht
	*v += 1;
}

void TestOp::ink2(uint8_t& v) {
	v++;
}

void TestOp::do1(void (*f)()) {
	f();
}

void TestOp::do2(void (&f)()) {
	f();
}
//*************************************************************


// methoden privat ********************************************

//*************************************************************

und zu guter letzt die ino-Datei zum Testen

#include "Arduino.h"
#include <_testOp.h>

TestOp var1(0,0);
TestOp var2(0,0);
TestOp var3(0,0);

uint8_t inkVar = 0;

void setup() {
	Serial.begin(115200);
	
	//Test 1 *****************************************************
	send_test1();			// --> Ausgabe: var1.x = 0, var1.y = 0
	var1++;
	send_test1();			// --> Ausgabe: var1.x = 1, var1.y = 1
	Serial.println();
	//************************************************** bestanden
	
	
	//Test 2 *****************************************************
	send_test1();			// --> Ausgabe: var1.x = 1, var1.y = 1
	++var1;
	send_test1();			// --> Ausgabe: var1.x = 2, var1.y = 2
	Serial.println();
	//************************************************** bestanden
	
	
	//Test 3 *****************************************************
	send_test1();			// --> Ausgabe: var1.x = 2, var1.y = 2
	send_test2();			// --> Ausgabe: var2.x = 0, var2.y = 0
	var2 = ++var1;
	send_test1();			// --> Ausgabe: var1.x = 3, var1.y = 3
	send_test2();			// --> Ausgabe: var2.x = 2, var2.y = 2
	Serial.println();
	//************************************************** bestanden
	
	
	//Test 4 *****************************************************
	send_test1();			// --> Ausgabe: var1.x = 3, var1.y = 3
	send_test2();			// --> Ausgabe: var2.x = 2, var2.y = 2
	var2 = var1++;
	send_test1();			// --> Ausgabe: var1.x = 4, var1.y = 4
	send_test2();			// --> Ausgabe: var2.x = 4, var2.y = 4
	Serial.println();
	//************************************************** bestanden
	
	
	//Test 5 *****************************************************
	send_test1();			// --> Ausgabe: var1.x = 4, var1.y = 4
	send_test2();			// --> Ausgabe: var2.x = 4, var2.y = 4
	send_test3();			// --> Ausgabe: var3.x = 0, var3.y = 0
	var3 = var1 + var2;
	send_test1();			// --> Ausgabe: var1.x = 4, var1.y = 4
	send_test2();			// --> Ausgabe: var2.x = 4, var2.y = 4
	send_test3();			// --> Ausgabe: var3.x = 8, var3.y = 8
	Serial.println();
	//************************************************** bestanden
	
	
	//Test 6 *****************************************************
	Serial.println(inkVar);	// --> Ausgabe: inkVar = 0
	var1.ink1(&inkVar);
	Serial.println(inkVar);	// --> Ausgabe: inkVar = 1
	var1.ink2(inkVar);
	Serial.println(inkVar);	// --> Ausgabe: inkVar = 2
	Serial.println();
	//************************************************** bestanden
	
	
	//Test 7 *****************************************************
	var1.do1(&f1);			// --> Ausgabe: f1
	var1.do2(f2);			// --> Ausgabe: f2
	//************************************************** bestanden
}

void loop() {
	
}

void send_test1() {
	Serial.print("var1 -> x: ");
	Serial.print(var1.x);
	Serial.print(", y: ");
	Serial.println(var1.y);
}

void send_test2() {
	Serial.print("var2 -> x: ");
	Serial.print(var2.x);
	Serial.print(", y: ");
	Serial.println(var2.y);
}

void send_test3() {
	Serial.print("var3 -> x: ");
	Serial.print(var3.x);
	Serial.print(", y: ");
	Serial.println(var3.y);
}

void f1() {
	Serial.println("f1");
}

void f2() {
	Serial.println("f2");
}

Das überladen der Operatoren klappt auch soweit super. Allerdings verstehe ich nicht, wieso ich bei dem überladen des + Operators den Aufruf in der Header-Datei (Zeile 17) als friend deklarieren muss. Außerdem muss ich in der cpp-Datei den Operator global deklarieren (Zeile 40) und nicht wie in Zeile 39 lokal, wie auch bei den anderen Operatoren. Hat jemand eine Erklärung dafür?
Irgendein Unterschied muss da ja sein.

Dann ist mir noch aufgefallen, dass ich den Inhalt eines Zeiger nicht mit *v++ inkrementieren kann. Mit *v+=1 funktioniert es.
Soll das so sein?

Letzte Frage. Ich habe mir zwei Methoden erstellt, um eine Variable zu inkrementieren, Zeile 47 und 52 in der cpp-Datei. Unterschied ist hier nur der übergene Parameter, bei ink1 ein Zeiger und bei ink2 die Speicheradresse.
Beides funktioniert, aber welche sollte man besser verwenden und warum? Eine Frage des Geschmacks? Bei einer Funktions-Übergabe funktionieren auch beide Methoden.

Das wars dann erstmal.

Schöne Grüße
Christian

Interessiert dich evtl.:

Der ++Prefix operator sollte optimalerweise als Refererenz TestOp[b]&[/b] operator++(); definiert werden, damit er das Element selbst zurückliefert und keine Kopie.

Probier mal den + Operator so:
TestOp operator+(TestOp const& other) const; ( will der VS2013 c++ Compiler so haben )

Sonst hab ich noch zu friend folgendes gefunden

Wird der Operator nicht mittels operator+= implementiert, benötigt er meist Zugriff auf private Attribute von X. In dem Fall wird er häufig als friend der Klasse deklariert oder greift auf eine öffentliche Methode der Klasse zu, die die eigentliche Operation ausführt:

const X X::plus(X const& rhs) const; //Implementiert die Addition

const X operator+(X const& lhs, X const& rhs)
{
  return lhs.plus(rhs);
}

https://www.c-plusplus.net/forum/232010-full

Christian81:
Dann ist mir noch aufgefallen, dass ich den Inhalt eines Zeiger nicht mit *v++ inkrementieren kann. Mit *v+=1 funktioniert es.
Soll das so sein?

Ja. Das dereferenziert die Variable und inkrementiert dann den Zeiger. Nicht den Inhalt.

Du willst das:

(*v)++

Danke an Euch beiden.

Die Antworten haben mir schon mal weitergeholfen.

Den + Operator habe ich jetzt umgeschrieben.
Funktioniert schonmal.

TestOp operator+(const TestOp &v1);
TestOp TestOp::operator+(const TestOp &v1) {
	TestOp v;
	v.x = x + v1.x;
	v.y = y + v1.y;
	return v;
}

So finde ich es schöner und ist auch den anderen Operatoren gleich
Es macht ja auch keinen Sinn, zwei Parameter zu übergeben, weil ich von der
ersten ja eine Kopie erstellen kann. Obwohl es auch funktioniert

Um den Inhalt eines Zeigers zu inkrementieren mit (v*)++ kannte ich noch nicht.
Funtioniert soweit auch.

Schönen Dank.
Gruß
Christian

Funktioniert schonmal.

Sicher ?

const TestOp c1 (1,2);
const TestOp c2 (3,4);

TestOp sum = c1+c2;

:wink:

Es scheint zwar egal zu sein, ob der Parameter (TestOp const & v) oder (const TestOp& v) heisst,
aber dass der operator+ *this nicht verändert, musst du auch definieren, damit mein Gegenbeispiel geht.

Unterschied ist hier nur der übergene Parameter, bei ink1 ein Zeiger und bei ink2 eine Referenz.
Beides funktioniert, aber welche sollte man besser verwenden und warum? Eine Frage des Geschmacks?

Beim Zeiger sieht man es leichter, dass ink1 wohl den Wert verändern wird, daher wäre nach meinem Geschmack diese Version besser.

Die Referenz-Version sieht netter aus, weil im folgenden mit dem Variablennamen gearbeitet wird statt mit (*ptr). Dass das aber keine ByValue Kopie ist, kriegt man nur bei genauerem Hingucken mit.

Hier in diesem Forum halte ich Referenzen für noch verwirrender als Zeiger. :wink:

Der Compiler wird vermutlich Referenzen genauso in Adressen umrechnen wie Zeiger, ist also wohl egal und eher eine Geschmacksfrage.

Besser ist, was kleineren Code produziert und dabei lesbar bleibt.
Daher solltest du auch nachmessen, ob deine Operatoren-Überlade-Spielereien "gut" sind.

Bei meiner Variante, die ich getestet habe, wurde der *this Zeiger nicht verändert.
Dein Gegenbeispiel hat allerdings nicht funktioniert.

const TestOp c1 (1,2);
const TestOp c2 (3,4);

TestOp sum = c1+c2;

Wenn ich jedoch die von Dir vorgeschlagene Variante nehme, funktioniert es.

TestOp operator+(TestOp const& other) const;

Diese ähnliche Variante funktioniert auch.

TestOp TestOp::operator+(const TestOp& other) const {

Der genaue Unterschied ist mir noch schleierhaft, da muss ich nochmal genauer recherchieren. Auch das const am Ende ist mir noch ein Rätsel.
Das Internet ist ja voll damit, aber es gibt die unterschiedlichsten Varianten dafür. Es ist schwer was zu finden wo nicht nur steht was man schreiben muss, sondern auch warum man es so macht und was jeder Bestandteil des Aufrufes bewirkt.

Ich habe mich jetzt für die friend Variante entschieden, man hat somit noch die Möglichkeit eine Klasse mit einem int-Wert zu addieren bzw. zu verarbeiten.
Das habe ich gestern Abend nochmal getestet.

friend TestOp operator+(const TestOp&, const TestOp&);
friend TestOp operator+(const TestOp&, int);
friend TestOp operator+(int, const TestOp&);

TestOp operator+(const TestOp& v1, const TestOp& v2) {
	TestOp v;
	v.x = v1.x + v2.x;
	v.y = v1.y + v2.y;
	return v;
}

TestOp operator+(const TestOp& v1, int v2) {
	TestOp v;
	v.x = v1.x + v2;
	v.y = v1.y + v2;
	return v;
}

TestOp operator+(int v1, const TestOp& v2) {
	TestOp v;
	v.x = v2.x + v1;
	v.y = v2.y + v1;
	return v;
}

Das scheint so zu funktionieren.

Besser ist, was kleineren Code produziert und dabei lesbar bleibt.
Daher solltest du auch nachmessen, ob deine Operatoren-Überlade-Spielereien "gut" sind.

Gerade das Erstellen von Klassen macht ja den eigenlichen Code lesbarer, vorausgesetzt man strukturiert die Klasse sinnvoll. Das Überladen von Operatoren in Klassen wird wohl eher Seltensheitswert haben, ist aber auch gegebenfalls sinnvoll. Bei einer Klasse für Komplexe Zahlen z.B liest es sich gut wenn eine Multiplikation mit complex1 *= complex2 durchgeführt wird. Und in einer Klasse eine gute Lesbarkeit zu erzielen ist ja auch nicht der Sinn und auch schlecht zu erzielen wenn dort alles mit Zeigern, überladen oder direkten Adresszugriffen erfolgt.
Ich habe mich erstmal mit dem Überladen von Operatoren beschäftigt um einen Lernerfolg zu erzielen.
Wann ich es das erset mal Sinnvoll anwende kann ich noch nicht sagen. Evtl. bekommen meine Regelkreis-Glieder Klassen einen solchen Nachtrag.

Dann vielen Dank für Eure Hilfe, dass hat mich ein gutes Stück weitergebracht.
Viele Güße und ein schönes Wochenende
Christain

Das const am Ende ist einmal ein Compiler Check, mit dem du markieren kannst, dass die Funktion nichts ändert (const Funktion). Damit teilst du dem Compiler mit was du beabsichtigst und er prüft ob die auch wirklich programmiert hast was du möchtest.

Die Signatur einer Funktion kann sich aber in const unterscheiden! Wo das wichtig ist, ist wenn man mit dem this-Zeiger arbeitet. Hier wäre vor allem der Subscript Operator [] genannt. Da kann man mit einer const-Funktion unterscheiden ob das Objekt geändert werden soll und dann je nachdem ob das Objekt const ist oder nicht eine const Referenz oder eine normale Referenz zurückgeben.

Auch das const am Ende ist mir noch ein Rätsel.

Ich hatte Verständnis - Schwierigkeiten mit Serenilfys Erklärung.

Ich verstehe das so:

Das const am Ende in der Deklaration versichert, dass das Objekt selbst durch die Funktion nicht verändert wird. Kann also auch auf const Objekte angewendet werden.

Wenn dieses const am Ende fehlt, meckert der Compiler, wenn die Funktion auf ein const Objekt angewendet wird.

Andersrum kann/darf man die Funktion natürlich nicht so schreiben, dass sie das Objekt selbst verändert.