Die Typlücke

von Hubert Schmid vom 2012-07-22

Die meisten Programmiersprachen unterscheiden in irgendeiner Form zwischen eingebauten und Benutzer-definierten Datentypen. Erstere besitzen in der Regel irgendwelche Eigenschaften, die sich mit Letzteren nicht realisieren lassen. Diese Trennung ist in vielen Fällen hinderlich und beschäftigt daher immer wieder die Sprachentwickler.

Es gibt grundsätzlich zwei Möglichkeiten, diese Zweiklassengesellschaft aufzuheben: Entweder erweitert man die Möglichkeiten der Benutzer-definierten Datentypen, um sie auf die obere Ebene zu heben. Oder man beschneidet die Fähigkeiten der eingebauten Datentypen und gleicht sie damit den Benutzer-definierten Typen an. Während die meisten Programmiersprachen letzteren Weg gehen, hatte C++ von Anfang an Ersteren zum Ziel.

Dieses Ziel ist in C++ bei Weitem noch nicht erreicht. Es ist beispielsweise nachwievor nicht möglich von eingebauten Datentypen abzuleiten. Trotzdem verkleinert sich die Lücke immer wieder ein wenig. Darauf möchte ich anhand meines Neun-mal-Sechs-Beispiels genauer eingehen.

6 * 9 = 42;

Diese Anweisung basiert auf dem Datentyp int und ist in C++ nicht korrekt, da eine Zuweisung an den Ausdruck auf der linken Seite nicht möglich ist. Diesen Ausdruck möchte ich auf einen Benutzer-definierten Datentyp decimal übertragen. Mit den folgenden beiden Deklarationen wird schon mal die Multiplikation und die Zuweisung unterstützt:

class decimal { // ... }; auto operator*(const decimal& lhs, const decimal& rhs) -> decimal;

Eine Neuerung von C++11 sind die Benutzer-definierten Literale. Die obigen int-Literale lassen sich dadurch relativ einfach durch passende decimal-Literale ersetzen. Die entsprechende Deklaration ist ganz unscheinbar:

auto operator "" _d(const char* data) -> decimal;

Diese Deklarationen sind bereits ausreichend um die folgenden Anweisungen übersetzen zu können:

decimal lhs = 6_d * 9_d; decimal rhs = 42_d; lhs = rhs;

Das sieht so weit ganz gut aus. Allerdings gibt es noch ein Problem: Die Anweisung 6_d * 9_d = 42_d ist leider ebenfalls gültig, was nicht sein sollte, da es mit eingebauten Datentypen nicht erlaubt ist. Aber auch für dieses Problem gibt es mit C++11 nun endlich eine einfache Lösung. Man muss lediglich die Zuweisungsoperatoren (für Copy und Move) auf L‑Value-Referenzen beschränken. Man achte im folgenden Code-Fragment auf das &, das auf die Parameterlisten der beiden Zuweisungsoperatoren folgt:

class decimal { private: // --- state --- // ... public: // --- con-/destruction --- decimal(const decimal& rhs) = default; decimal(decimal&& rhs) noexcept = default; ~decimal() = default; public: // --- operations --- auto operator=(const decimal& rhs) & -> decimal& = default; auto operator=(decimal&& rhs) & noexcept -> decimal& = default; };

Mit diesen Deklarationen liefert der Compiler bei der Zuweisung eine Fehlermeldung. Diese ist bei Clang leider noch nicht optimal formuliert, sagt im Wesentlichen aber aus, dass kein Zuweisungsoperator gefunden werden konnte, der zu den Typen auf der linken und rechten Seite des Gleichheitszeichens passt.

error: no viable overloaded '=' 6_d * 9_d = 42_d; ~~~~~~~~ ^ ~~ note: candidate function not viable: \ no known conversion from 'decimal' to 'decimal' for object argument; auto operator=(decimal&& rhs) & noexcept -> decimal& = default; ^ note: candidate function not viable: \ no known conversion from 'decimal' to 'decimal' for object argument; auto operator=(const decimal& rhs) & -> decimal& = default; ^

Mit diesen Erweiterungen ist C++ dem Ziel eines einheitlichen Metamodells für Datentypen mal wieder ein paar Schritte näher kommen. Und wäre die Abwärtskompatibilität zu C kein weiteres Ziel von C++, so wäre die Typlücke möglicherweise bereits vollständig geschlossen.