C++11 und Perfect Forwarding

von Hubert Schmid vom 2011-12-11

Eine häufig genannte Neuerung von C++11 ist das sogenannte Perfect Forwarding. Damit ist im Wesentlichen gemeint, dass man Template-Funktionen schreiben kann, die ihre Argumente unverändert an andere Funktionen weiterreichen können. Am einfachsten kann ich das an einem Beispiel erklären.

class foo { ... }; int main() { auto ptr = std::unique_ptr<foo>{ new foo{some-expr, some-other-expr}}; }

Die Aufgabe besteht darin, eine Template-Funktion zu schreiben, die für beliebige Typen die Initialisierung des Typs und des std::unique_ptr kombiniert, wobei sich der Aufruf dieser Factory-Funktion genauso verhalten soll, als hätte man die beiden Operationen direkt ausgeführt. Gesucht ist also eine Funktion make_unique mit der sich das oben stehende Beispiel folgendermaßen umschreiben lässt, ohne dass sich dabei die Semantik ändert.

class foo { ... }; int main() { auto ptr = make_unique<foo>(some-expr, some-other-expr); }

Vor C++11 waren solche generischen Funktionen nur sehr eingeschränkt – oder zumindest sehr umständlich – möglich. Die neue Lösung für dieses Problem nennt sich Perfect Forwarding. Und dabei handelt es sich nicht um ein einzelnes Feature, sondern um eine ganze Reihe zusammenspielender Erweiterungen.

R-Value-References

Ich will an dieser Stelle nicht auf die Details zu R‑Values und R‑Value-References eingehen. Wichtig für das Verständnis ist hauptsächlich, dass mit den neuen Referenzen zwischen L- und R‑Values sinnvoll unterschieden werden kann.

const int& bar = 42; int&& baz = 42; // NOTE: C++11 r-value reference

Die erste Zeile aus diesem Beispiel war in C++ schon immer möglich. Neu ist hingegen die zweite Zeile, bei der die temporäre Zwischenvariable an eine nicht-konstante R‑Value-Reference gebunden wird.

Template-Instantiierung

Der nächste Schritt besteht darin, eine Template-Funktion zu schreiben, deren Parameter mit einem beliebigen Referenztyp instanziiert werden kann. Durch die R‑Value-References und ein paar Anpassungen der Regeln für die Instantiierung ist das nun sehr einfach möglich.

template <typename Arg> void make_unique(Arg&& arg);

Auf den ersten Blick könnte man meinen, dass diese Funktion nur R‑Value-References akzeptiert. Tatsächlich kann diese Funktion aber mit beliebigen Argumenten aufgerufen werden. Denn wird für den Template-Parameter eine L‑Value-Referenz eingesetzt, so wird auch der Parametertyp zu einer L‑Value-Referenz.

Der Grund dafür ist das sogenannte Reference-Collapsing. In C++03 war es nicht erlaubt, mehrere Referenzen zu kombinieren. Das hat sich mit C++11 nun geändert. Bei der Kombination entstehen allerdings keine Referenzen auf Referenzen. Stattdessen legt ein Regelwerk fest, wie mehrere Referenzen zu einer einfachen Referenz zerfallen. Die Regeln sind auf den ersten Blick nicht unbedingt einleuchtend. Für dieses Beispiel ist aber nur relevant, dass eine L‑Value-Referenz zusammen mit einer anderen Referenz wieder eine L‑Value-Referenz ergibt.

std::forward

Als nächstes muss der Parameter an die eigentliche Funktion übergeben werden. Das hört sich trivial an, aber man muss auf eine Besonderheit der R‑Value-References achten.

template <typename Type, typename Arg> void make_unique(Arg&& arg) { new Type{arg}; // NOTE: arg is an l-value reference }

Der oben stehende Code leistet das Gewünschte nämlich nicht. Das liegt daran, dass benannte Variablen unabhängig von ihrer Deklaration immer L‑Value-References sind, und das natürlich aus gutem Grund. Abhilfe schafft hier ein Cast auf den gewünschten Typ. Genau für diesen Anwendungsfall gibt es das Funktionstemplate std::forward, das diese Aufgabe übernimmt und darüber hinaus die eigentliche Absicht deutlich macht.

template <typename Type, typename Arg> void make_unique(Arg&& arg) { new Type{std::forward<Arg>(arg)}; }

Variadic Templates

Jetzt fehlt nur noch ein Schritt bis zum Ziel: Die Factory-Funktion soll mit einer beliebigen Anzahl Parameter funktionieren. Und diese Aufgabe lösen die neu eingeführten Variadic Templates.

template <typename Type, typename... Args> auto make_unique(Args&&... args) -> std::unique_ptr<Type> { return std::unique_ptr<Type>{ new Type{std::forward<Args>(args)...}}; }

Und fertig ist das Funktionstemplate zur Konstruktion eines Objekts mit Smart-Pointer, das wie oben gezeigt eingesetzt werden kann, den Code einfacher und lesbare gestaltet und praktisch keinen Overhead mit sich bringt.

Weitere Beispiele

Die Standardbibliothek enthält einige weitere Beispiele für Perfect Forwarding, an denen man ein paar Einsatzmöglichkeiten erkennen kann. Typisch ist vermutlich die Indirektion beim Funktionsaufruf, wie sie beispielsweise beim std::reference_wrapper der Fall ist.

// identical from the callee's point of view callable(some-expr, some-other-expr); std::ref(callable)(some-expr, some-other-expr);

Davon unterscheidet sich std::make_tuple ein wenig. Dort wird das Perfect Forwarding eingesetzt, um jeden einzelnen Member mit dem richtigen Copy- oder Move-Konstruktor zu initialisieren. Ganz neue Möglichkeiten schaffen dagegen die emplace-Funktionen der Standard-Container. Mit diesen Funktionen kann ein Objekt direkt im Container konstruiert werden, ohne dass es dorthin kopiert oder verschoben werden muss. Das bedeutet beispielsweise, dass eine std::list auch verwendet werden kann, um Objekte zu speichern, die weder kopierbar noch verschiebbar sind.

std::list<std::thread> threads; threads.emplace_back(callable, arg1, arg2);

In diesem Beispiel wird innerhalb der Liste ein Knoten mit dem Konstruktor std::thread{callable,arg1,arg2} erzeugt. Und auch bei kopierbaren Datentypen wird dadurch eine unnötige Operation vermieden und der Code kompakter.

Fazit

Das Perfect-Forwarding bietet weitere Möglichkeiten, um in C++ Abstraktionen in Form von Template-Klassen und -Funktionen zu schaffen, die in ihrer Verwendung einfach, sicher und effizient sind. Anwendung wird dieses Feature vermutlich hauptsächlich in Bibliotheken finden. Die Anwendungsentwickler werden durch bessere und allgemeiner einsatzbarere Bibliotheken davon profitieren.

Die Erweiterungen der Standardbibliothek geben einen ersten Vorgeschmack darauf. Ich gehe allerdings davon aus, dass die Einsatzbereiche wesentlich umfangreicher sind. Und ich bin sehr gespannt darauf, was im Rahmen der Boost-Bibliothek daraus gemacht wird.