C++11 und Funktionen mit beliebiger Anzahl Argumente gleichen Typs

von Hubert Schmid vom 2012-04-01

Schon seit Ewigkeiten unterstützt die Programmiersprache C – und damit auch C++ – Funktionen mit variabler Anzahl Argumente. Das bekannteste Beispiel dafür ist sicherlich printf. Ermöglicht wird die Implementierung solcher Funktionen durch die sogenannte Ellipse und die Makros aus dem Header <stdarg.h>. Allerdings bringt deren Verwendung einen großen Nachteil mit sich. Die Typinformationen gehen beim Funktionsaufruf verloren. Und damit ist dieses Vorgehen für komplexe Datenstrukturen nur sehr bedingt geeignet.

Mit C++11 gibt es nun gleich mehrere alternative Realisierungsmöglichkeiten solcher Funktionen. Im Gegensatz zu der Umsetzung in anderen, statisch typisierten Programmiersprachen werden dabei auch statisch variable Typen unterstützt, wie sie beispielsweise für printf sinnvoll sind. In diesem Artikel beschränke ich mich aber zunächst auf die Übergabe einer variablen Anzahl Argumente des gleichen Typs. Für komplexere Anwendungen mit unterschiedlichen Typen muss ich auf einen späteren Artikel vertrösten.

Ich habe mir ein sehr einfaches Beispiel herausgesucht: Es soll eine Funktion geschrieben werden, die eine beliebige Anzahl Argumente akzeptiert, aus denen std::string-Objekte erzeugt werden können. Diese Zeichenketten werden zusammengehängt und als Ergebnis zurückgegeben. Die Verwendung sollte ungefähr so aussehen:

std::string message = concat("Hello", " ", "World", "!"); std::cout << message << '\n';

std::initializer_list

Meine erste Implementierung setzt auf std::initializer_list und die zugehörige Sprachunterstützung. Der große Vorteil bei deren Verwendung ist, dass der Code besonders einfach zu implementieren und verstehen ist. Der Anwender kann das Klassen-Template mit beliebig vielen Argumenten initialisieren und im Rumpf der Funktion kann der Parameter praktisch wie ein unveränderlicher Container verwendet werden.

auto concat(std::initializer_list<std::string> args) -> std::string { std::string result; for (auto&& arg : args) { result.append(arg); } return result; }

Die Implementierung hat allerdings ein paar Nachteile, die man beachten sollte. Einerseits ist sie unnötig ineffizient, weil jeder std::string einmal kopiert wird – sofern nicht der Move-Konstruktion verwendet werden kann. Und anderseits sieht die Verwendung beim Aufrufer ein wenig anders als gewünscht aus. Die Argumente müssen nämlich beim Aufruf in geschweifte Klammern eingeschlossen werden.

std::string message = concat({ "Hello", " ", "World", "!" });

Variadic Templates ohne Rekursion

Die zweite Implementierung beseitigt das Problem der unerwünschten geschweiften Klammern und ist auch nur unwesentlich länger. Sie verwendet allerdings das neue Sprachkonstrukt für die sogenannten Variadic Templates, das nicht ganz trivial zu verstehen ist.

template <typename ...Args> auto concat(Args&&... args) -> std::string { std::initializer_list<std::string> args_{ std::forward<Args>(args)... }; std::string result; for (auto&& arg : args_) { result.append(arg); } return result; }

Bei dem Parametertyp steht ... für eine gepackte Typliste, die nur zur Übersetzungszeit existiert. Bei der Initialisierung der lokalen Variablen in der folgenden Zeile bedeutet ..., dass der vorherige Ausdruck für jedes Element der gepackten Typliste durch den Übersetzer wieder expandiert wird. Die Kombination mit Perfect-Forwarding sorgt dafür, dass keine Typinformationen bei der Weiterleitung verloren gehen.

Die Funktion macht also im Wesentlichen das Gleiche wie die erste Implementierung, wobei die Indirektion über die Variadic Templates lediglich verwendet wird, um die Argumente ohne geschweifte Klammern übergeben zu können. Anstatt std::initializer_list hätte ich auch einen anderen Container wie beispielsweise std::vector verwenden können. Allerdings wären damit einige unintuitive Aufrufe möglich gewesen. Beispielsweise hätte der Aufrufe concat(3, "foobar") die als zweites Argument übergebene Zeichenkette dreimal hintereinander gehängt.

Variadic Templates mit Rekursion

Die zweite Implementierung konnte zwar das Problem mit den geschweiften Klammern beim Aufruf lösen, die unnötigen Kopieroperationen blieben aber erhalten. Um auch diesen Punkt zu adressieren verwende ich jetzt die Variadic Templates in der Form, wie man sie typischerweise sieht – in einer rekursiven Definition.

template <typename ...Args> auto concat(Args&&... args) -> std::string { std::string result; concat_aux(result, std::forward<Args>(args)...); return result; }

Die Implementierung besteht aus zwei Teilen. Die aufgerufene Funktion erzeugt den Kontext, der für die effiziente, rekursive Abarbeitung benötigt wird. Anschließend wird der Kontext zusammen mit den Argumenten (Perfect-Forwarding) an eine interne Hilfsfunktion weitergereicht.

void concat_aux(std::string& result) { (void) result; // nothing to do } template <typename ...Tail> void concat_aux(std::string& result, const std::string& head, Tail&&... tail) { result.append(head); concat_aux(result, std::forward<Tail>(tail)...); }

Die Hilfsfunktion ist rekursiv definiert. Im Fall einer leeren Parameterliste ist nichts zu tun. Existiert mindestens ein Parameter, so wird dessen Wert verarbeitet und die verbleibenden Argumente rekursiv abgearbeitet.

Die Implementierung kann genauso wie die vorherige Implementierung verwendet werden, vermeidet aber die Kopieroperationen, da die std::string-Objekte immer nur per Referenz übergeben werden. Allerdings muss man sich nun mit der rekursiven Definition anfreuden.

Variadic Templates mit Schleife

Die vorherige Implementierung ist einfach zu verwenden und sehr effizient. Allerdings ist sie aufgrund der rekursiven Definition nicht so leicht zu verstehen, wie die beiden ersten Implementierungen. Daher werde ich in der vierten und letzten Variante die Rekursion durch eine Art Schleife ersetzen. Eine normale Schleife ist an dieser Stelle nicht möglich, da Schleifen Laufzeitkonstrukte sind und sich nicht mit Variadic Templates vertragen, die nur zur Übersetzungszeit existieren. Stattdessen baue ich ein Funktionstemplate, das die Schleife ersetzt und vergleichbar zu std::for_each ist.

template <typename Callable> void variadic_for_each(Callable&& callable) { (void) callable; // nothing to do } template <typename Callable, typename Head, typename ...Tail> void variadic_for_each(Callable&& callable, Head&& head, Tail&&... tail) { callable(std::forward<Head>(head)); variadic_for_each(std::forward<Callable>(callable), std::forward<Tail>(tail)...); }

Dieses Funktionstemplate erwartet als erstes Argument ein Funktionsobjekt und ruft Letzteres der Reihe nach mit allen Argumenten auf. Dabei gibt es keine Einschränkungen an die Typen, so dass es sehr allgemein eingesetzt werden kann. Mit dieser Hilfskonstruktion lässt sich die concat-Funktion nun folgendermaßen implementieren:

template <typename ...Args> auto concat(Args&&... args) -> std::string { std::string result; auto&& append = [&](const std::string& v) { result.append(v); }; variadic_for_each(append, std::forward<Args>(args)...); return result; }

Das Funktionsobjekt wird dabei durch eine Lambda-Funktion realisiert, die ich der Lesbarkeit wegen in einer separaten Zeile definiert habe. Aber auf jeden Fall wird dadurch die Rekursion verdeckt. Und auch wenn es nicht danach aussieht, so ist die Funktion genauso effizient wie die vorherige Implementierung, da praktisch alles bereits zur Übersetzungszeit vollständig expandiert wird.

Fazit

C++11 bietet so viele unterschiedliche Möglichkeiten Funktionen zu implementieren, die eine beliebige Anzahl Argumente akzeptieren, dass man gar nicht genau weiß, welche man verwenden soll. In diesem Artikel habe ich ein paar Varianten aufgezeigt, die einfach und sicher sind, und sich für produktiven Code eignen. Darüber hinaus existieren noch einige weitere Alternativen. Welche Implementierung in welchem Fall eingesetzt werden sollte, lässt sich zum jetzigen Zeitpunkt noch nicht sagen. Diese Erfahrungen müssen erst noch gesammelt und ausgewertet werden. Klar ist allerdings, dass sich die Situation im Vergleich zu <stdarg.h> signifikant verbessert hat.