C++ Core Guidelines: Überladen von Funktionen und Operatoren

Modernes C++  –  2 Kommentare

Die Guidelines besitzen zehn Regeln für das Überladen von Funktionen und Operatoren. Viele von ihnen sind recht naheliegend. Werden sie aber nicht eingehalten, birgt die Software viele Überraschungen.

Ich bin sehr verwundert, dass es nur zehn Regeln für das Überladen in den Guidelines gibt. Verwundert, da ich in der Vergangenheit sehr häufig Diskussion zum Überladen von Operatoren in C++ geführt habe. Zusätzlich kommt hinzu, dass MISRA C++, das sehr häufig in Automobilbereich und der Embedded-Entwicklung eingesetzt wird, das Überladen von Operatoren verbietet.

Im Gegensatz zu der intensiven Diskussion zum Überladen von Operatoren in C++ habe ich keine Diskussion dazu in Python im Gedächtnis. Dies gilt, obwohl das Überladen von Operatoren in Python sehr mächtig und zugleich idiomatisch ist. Betrachte nur die vielen speziellen Methoden, die mit zwei Unterstrichen beginnen und enden und in Python liebevoll dunder genannt werden. Diese musst du implementieren, um einen Datentyp zu erhalten, der sich wie ein int verhält.

Nun geht es aber los mit C++. Hier sind die zehn Regeln.

Wenn die Regel recht offensichtlich ist, werde ich es in bekannter Manier kurz halten.

C.160: Define operators primarily to mimic conventional usage

Du solltest dem Prinzip der kleinsten Überraschung folgen. Oder um es in den Worten von C:61 auszudrücken: A copy operation should be copy. Das heißt, nach der Zuweisung x = y muss gelten: x == y.

Das war recht offensichtlich. Die nächste Regel hört sich ziemlich einfach an. Ihre Diskussion bringt aber interessante Erkenntnisse ans Licht.

C.161: Use nonmember functions for symmetric operators

In der Regel ist die Implementierung eines symmetrischen Operators in einer Klasse nicht möglich. Das zeigt relativ schnell der Operator +.

Nimm daher an, dass du eine Klasse MyInt implementieren willst, die die Addition mit dem fundamentalen Datentyp int unterstützen soll. Hier ist die erste Umsetzung der Anforderung.

// MyInt.cpp

struct MyInt{
MyInt(int v):val(v){}; // 1
MyInt operator+(const MyInt& oth) const {
return MyInt(val + oth.val);
}
int val;
};

int main(){

MyInt myFive = MyInt(2) + MyInt(3);
MyInt myFive2 = MyInt(3) + MyInt(2);

MyInt myTen = myFive + 5; // 2
MyInt myTen2 = 5 + myFive; // 3 ERROR

}

Dank des impliziten Konvertierungskonstruktors (1) ist der Ausdruck (2) gültig. Das gilt aber nicht für den Ausdruck (3), denn die 5 in dem Ausdruck 5 + myFive wird nicht automatisch nach MyInt konvertiert. Das Ergebnis ist ein Compilerfehler.

Genauer gesagt, scheitert das Kompilieren des Programms, da der fundamental Datentyp int den Operator + nicht für MyInt überladen hat.

Das kleine Programm besitzt gleich mehrere Probleme.

  1. Der + Operator ist nicht symmetrisch.
  2. Die val-Variable ist public.
  3. Der Konvertierungskonstruktor ist implizit.

Es ist einfach, die ersten zwei Probleme zu lösen, indem ein freier +-Operator zum Einsatz kommt, der in der Klasse als friend deklariert wird.

// MyInt2.cpp

class MyInt2{
public:
MyInt2(int v):val(v){};
friend MyInt2 operator+(const MyInt2& fir, const MyInt2& sec){
return MyInt2(fir.val + sec.val);
}
private:
int val;
};

int main(){

MyInt2 myFive = MyInt2(2) + MyInt2(3);
MyInt2 myFive2 = MyInt2(3) + MyInt2(2);

MyInt2 myTen = myFive + 5;
MyInt2 myTen2 = 5 + myFive;

}

Nun springt die implizite Konvertierung von int nach MyInt ein, und die Variable val ist privat. Dem Wortlaut der Regel C.164: Avoid conversion operators folgend, gilt aber, das du keinen impliziten Konvertierungskonstruktor anwenden sollst. Wende ich aber einen expliziten Konvertierungskonstruktor an, dann übersetzt das Programm nicht mehr.

// MyInt3.cpp

class MyInt3{
public:
explicit MyInt3(int v):val(v){}; // 1
friend MyInt3 operator+(const MyInt3& fir, const MyInt3& sec){
return MyInt3(fir.val + sec.val);
}
private:
int val;
};

int main(){

MyInt3 myFive = MyInt3(2) + MyInt3(3);
MyInt3 myFive2 = MyInt3(3) + MyInt3(2);

MyInt3 myTen = myFive + 5; // 2
MyInt3 myTen2 = 5 + myFive; // 3

}

Dank des expliziten Konvertierungskonstruktors (1) ist die implizite Konvertierung von int to MyInt nicht zulässig und die Zeilen (2) und (3) führen zu einem Fehler.

Zumindest folgte ich der Regel und der Operator + ist symmetrisch.

Der naheliegendste Weg, die ursprüngliche Anforderung umzusetzen, ist es, zwei zusätzliche +-Operatoren für MyInt4 anzubieten. Einer nimmt int als linkes, einer nimmt int als rechtes Argument an.

// MyInt4.cpp

class MyInt4{
public:
explicit MyInt4(int v):val(v){}; // 1
friend MyInt4 operator+(const MyInt4& fir, const MyInt4& sec){
return MyInt4(fir.val + sec.val);
}
friend MyInt4 operator+(const MyInt4& fir, int sec){
return MyInt4(fir.val + sec);
}
friend MyInt4 operator+(int fir, const MyInt4& sec){
return MyInt4(fir + sec.val);
}
private:
int val;
};

int main(){

MyInt4 myFive = MyInt4(2) + MyInt4(3);
MyInt4 myFive2 = MyInt4(3) + MyInt4(2);

MyInt4 myTen = myFive + 5; // 2
MyInt4 myTen2 = 5 + myFive; // 3

}

C.162: Overload operations that are roughly equivalent und C.163: Overload only for operations that are roughly equivalent

Die zwei Regeln lassen sich einfach zusammenfassen. Äquivalente Funktionen sollten die gleichen Namen besitzen. Oder anders herum. Nichtäquivalente Funktionen sollten nicht den gleichen Namen besitzen.

Hier ist ein Beispiel aus den Guidelines.

void print(int a);
void print(int a, int base);
void print(const string&);

Der Aufruf der Funktion print(arg) fühlt sich wie generisches Programmieren an. Du musst dir keine Gedanken dazu machen, welche Version von print verwendet wird .

Das gilt aber nicht für die drei nächsten Funktionen. Sie besitzen verschiedene Namen.

void print_int(int a);
void print_based(int a, int base);
void print_string(const string&);

Falls nichtäquivalente Funktionen den gleichen Namen besitzen, dann sind die Namen wohl zu allgemein oder schlichtweg falsch. Dies ist verwirrend und fehleranfällig.

Die nächste Regel ist sehr wichtig: C.164: Avoid conversion operators. Ich habe auf sie bereits in der Regel C.161 Bezug genommen. Du sollst keine impliziten Konvertierungskonstruktor und – das ist neu – keinen impliziten Konvertierungsoperator verwenden. Genau darüber werde ich im nächsten Artikel schreiben.

In der letzten Woche habe ich die zweite Auflage meines Buchs "The C++ Standard Library" veröffentlicht. Dieses Update deckt auch den neuen C++17-Standard ab. Auf Leanpub gibt es weitere Informationen zum Buch.