C++11 und Typinferenz für lokale Variablen

von Hubert Schmid vom 2011-11-20

Typinferenz für lokale Variablen in statisch typisierten Programmiersprachen ist eigentlich eine relativ einfache Sache. Aber es hat lange gedauert, bis dieses Feature in die wichtigsten Vertreter ihrer Gattung eingezogen ist. In C# wurde die Funktionalität mit der Version 3.0 eingeführt. In C++ hat es bis 2011 gedauert. Und Java-Entwickler müssen vermutlich noch bis 2014 darauf warten.

In C++ wird das Schlüsselwort auto für die lokale Typinferenz wiederverwendet. Bei der Variableninitialisierung innerhalb eines Code-Blocks kann es anstelle des Typs angegeben werden. In diesem Fall leitet der Compiler den Typ aus dem Initialisierer ab und ersetzt sozusagen das auto on-the-fly durch den passenden Typ. Das gesamte Programm verhält sich so, als hätte der Entwickler genau diesen Typ selbst eingesetzt.

void say_hello(std::string name) { // auto will be instantiated with std::string auto greeting = "Hello, " + name + '!'; std::cout << message << '\n'; // auto will be instantiated with int auto answer = 42; std::cout << "The answer is " << answer << ".\n"; }

Ich erinnere mich an eine Diskussion mit einem Kollegen, der einige Vorbehalte gegen diese Art der Typinferenz hatte. Aus seiner Sicht sprachen vor allem die erhöhte Komplexität der Sprache und der Einfluss auf die Lesbarkeit aufgrund der fehlenden Typen gegen dieses Feature.

Ich kann diese Probleme grundsätzlich schon verstehen, aber in dem speziellen Fall der Typinferenz – wie sie mit C++11 realisiert wurde – sehe ich sie nicht. Denn erstens ist dieses Feature im Gegensatz zu vielen bestehenden Eigenschaften der Sprache nun wirklich trivial. Und zweitens kann man so gut wie jedes Feature missbrauchen und damit die Lesbarkeit beeinträchtigen.

Umgekehrt: Den großen Vorteil der Typinferenz sehe ich gerade in der verbesserten Lesbarkeit. Bei richtigem Einsatz fokussiert man sich und den Leser auf die wesentlichen Informationen und abstrahiert von unwichtigen Details. Ich werde dazu gleich ein paar Beispiele zeigen, starte aber zunächst mit dem einfachsten Fall:

contract.set_customer_id(customer.get_customer_id());

In diesem Beispiel wird über eine Kunden-Kennung die Verbindung zwischen Kunde und Vertrag hergestellt. Dabei kommt keine Typinferenz zum Einsatz, aber ähnlich wie bei der Typinferenz wird der Typ der Kunden-Kennung nicht explizit angegeben. Das ist in diesem Fall auch gut so, denn aus fachlicher Sicht interessiert uns an dieser Stelle nicht, ob als Kunden-Kennung eine Zahl, eine Zeichenkette oder ein benutzerdefinierter Typ verwendet wird. Was ist aber, wenn wir diesen Ausdruck aus irgendwelchen Gründen in zwei Anweisungen aufteilen wollen?

auto customer_id = customer.get_customer_id(); contract.set_customer_id(customer_id);

In diesem Fall hilft die Typinferenz. Sie bringt zum Ausdruck, dass der Typ der Kunden-Kennung unbedeutend ist, vermeidet unbeabsichtigte, implizite Konvertierungen und macht den Code robust gegenüber Änderungen.

Bei der Kunden-Kennung würde die Angabe des Typs die Lesbarkeit vermutlich nicht sonderlich beeinträchtigen. Das sieht ganz anders aus, wenn die Typangabe relativ lang ist, aber kaum Information trägt. Typisch dafür sind verschachtelte Typen im Zusammenhang mit Templates. Das folgende Beispiel verwendet auto für die beiden lokalen Variablen. Mit dem tatsächlichen Typ würden die Anweisungen kaum in einzelne Zeilen passen und die wesentliche Information würde am rechten Rand im Rauschen untergehen.

auto join(const std::vector<std::string>& values) -> std::string { auto first = values.begin(); auto last = values.end(); // instead of: // std::vector<std::string>::const_iterator first = values.begin(); // std::vector<std::string>::const_iterator last = values.end(); if (first != last) { std::ostringstream os; os << *first; for (++first; first != last; ++first) { os << ' ' << *first; } return os.str(); } else { return std::string{}; } }

Die Funktion verwendet zwar einen instanziierten, generischen Datentyp, ist selbst aber nicht generisch. In generischen Funktionen wird die Typangabe nochmals ein wenig komplizierter. Eine generische Variante der oben stehenden Funktion würde beispielsweise wie folgt aussehen, wenn sie auf auto verzichten muss.

template <typename Range, typename Separator> auto join(Range&& range, Separator&& separator = ' ') -> std::string { using std::begin; using std::end; typename std::decay<decltype(begin(range))>::type first = begin(range); typename std::decay<decltype(end(range))>::type last = end(range); // ... }

Die Grundlagen für die Typinferenz sollten mit diesen Beispielen hinreichend klar geworden sein. Aber gerade das zuletzt gezeigte Beispiel deutet durch das std::decay darauf hin, dass ich ein paar wichtige Informationen bisher unterschlagen habe: Durch welchen Typ wird das auto instanziiert, wenn der Typ des Ausdrucks auf der rechten Seite cv-qualifiziert ist? Und was passiert eigentlich mit den L- und R‑Values?

Die kurze Antwort ist, dass das Schlüsselwort auto in diesem Zusammenhang qualifiziert werden kann, sowohl mit const und volatile als auch mit Referenzen und Zeigern. Und die Inferenz-Regeln für auto entsprechen genau den Regeln für die Inferenz der Typparameter bei Template-Funktionen. Aus diesem Grund wird man häufig auch die Variante auto&& sehen, um eine unnötige oder möglicherweise sogar unmögliche Kopie zu vermeiden. Die Anweisung do_something(some-expression); würde man dann wie folgt refaktorisieren.

auto&& intermediate = some-expression; do_something(intermediate);

Aber Vorsicht: Auch diese Refaktorisierung kann die Semantik des Programms auf subtile Weise ändern. Das liegt allerdings nicht an auto, sondern an den Eigenschaften von R‑Value-Referenzen und wann diese zu L‑Value-Referenzen werden.

Selbstredend sollte man die Typinferenz jetzt nicht an allen Stellen dogmatisch einsetzen, an denen sie möglich ist. Man benötigt ein wenig Erfahrung bis man ein Gefühl für die richtige Verwendung entwickelt hat, so dass sich tatsächlich die Lesbarkeit und Qualität des Codes nachhaltig verbessert. Aber ich bin fest davon überzeugt, dass das den meisten Entwicklern schnell gelingt, dass die Typinferenz in der breiten Masse die notwendige Akzeptanz finden wird, und dass das für viele Projekte ein Grund sein wird, auf die neue C++‑Sprachversion zu wechseln.