C++11 und std::initializer_list
Mit der Sprachversion 2011 hat sich in C++ einiges im Bereich der Initialisierung von Datenstrukturen getan. Die meisten Änderungen sind spezifisch für C++. Und da es dabei sehr ins Detail geht, möchte ich an dieser Stelle auch gar nicht weiter darauf eingehen. Es gibt aber eine Erweiterung, die ich besonders interessant finde – auch für Entwickler verwandter Programmiersprachen, denn dort beobachte ich ebenfalls den Wunsch für entsprechende Erweiterungen.
In C und C++ können die eingebauten Arrays relativ einfach initialisiert werden. Das funktioniert sowohl für primitive
als auch für benutzerdefinierte Datentypen. Und auch implizite Typkonvertierungen sind dabei kein Problem.
int primes[] = { 2, 3, 5, 7, 11, 13, 17, 19, };
// NOTE: implicit conversion from char* to std::string
std::string tokens[] = { "Hello", "World", };
Das funktioniert, weil die Sprache diese Syntax für die Initialisierung von Arrays direkt unterstützt. Benutzerdefinierte Container wie std::vector
und std::list
ließen sich aber bisher nicht entsprechend initialisieren. Alternativen dazu gibt es einige – angefangen beim einzelnen Einfügen der Elemente mit push_back
bis hin zu komplexen Hilfsfunktionen und -Klassen, um den syntaktischen Overhead zu reduzieren. In diese Kategorie fällt auch die Bibliothek boost::assign
. Und eine std::list
kann damit beispielsweise so initialisiert werden.
using boost::assign::list_of;
std::list<int> primes = list_of(2)(3)(5)(7)(11)(13)(17)(19);
Mit C++11 wird die Sache nun deutlich einfacher. Die Sprache unterstützt eine neue Form der Initialisierung mit einer variablen Anzahl Parameter gleichen Typs. Und bei der Syntax hat man sich an der existierenden Syntax für die Initialisierung von Arrays orientiert, wodurch sie relativ intuitiv wirkt. Das gerade gezeigte Beispiel lässt sich nun ohne weitere Hilfsfunktionen wie folgt schreiben.
std::list<int> primes = { 2, 3, 5, 7, 11, 13, 17, 19, };
Die gleiche Syntax funktioniert auch mit anderen Datentypen – wie beispielsweise std::vector
oder std::set
. Und selbstverständlich lässt sich diese Art der Initialisierung auch für eigene Datentypen nutzen. Aber dazu später mehr.
Interessant ist an dieser Stelle noch, dass die Syntax nicht auf die Deklaration lokaler Variablen beschränkt ist. Die Syntax funktioniert praktisch an allen Stellen, an denen eine Datenstruktur initialisiert wird. Und nicht ganz offensichtlich – aber äußert praktisch – finde ich in diesem Zusammenhang die Initialisierung von Funktionsparametern. So lässt sich beispielsweise die folgende Funktion einfach mit join({ "Hello", "World", }, ' ')
aufrufen.
auto join(const std::vector<std::string>& tokens, char separator)
-> std::string;
Bis hierher war alles ganz einfach. Aber die richtig interessanten Fälle fehlen noch. Dazu gehören insbesondere Datenstrukturen, die sich nicht über eine einfache Liste initialisieren lassen. Und ein typischer Vertreter dafür ist std::map
.
In Python wird eine Map mit der folgenden Syntax erzeugt. Und ich gehe davon aus, dass die Initialisierung in Java8 so ähnlich aussehen wird, falls Map-Literale dort tatsächlich Einzug halten.
# Python: creating a map and passing it to a function:
# def do_something(arg): ...
do_something({ 1: 'one', 2: 'two', 3: 'three', })
In C++11 sieht die Initialisierung hingegen weniger hübsch aus – und das vermutlich nicht nur auf den ersten Blick.
// C++11: initializing the parameter of a function:
// void do_something(std::map<int, std::string> arg);
do_something({ { 1, "one" }, { 2, "two" }, { 3, "three" }, });
Das sieht nicht nur anders aus als in Python, sondern es ist meiner Meinung nach auch schlechter lesbar. Aber natürlich gibt es einen guten Grund für diese Syntax. Im Gegensatz zu Python ist der Ausdruck für die Initialisierung der Map nämlich nicht spezifisch für Maps. Stattdessen werden in C++ zwei verwandte und eigenständige Konzepte miteinander kombiniert. Und wiederum orientiert man sich dabei an der Syntax, die bereits seit langem existiert.
struct map_entry
{
int key;
const char* value;
};
struct map_entry map[] = { { 1, "one" }, { 2, "two" }, { 3, "three" }, };
Das ist korrekter C‑Code, und funktioniert damit auch in C++. Man achte darauf, dass die Initialisierung der Array-Variable praktisch genauso wie oben die Initialisierung der std::map
aussieht. Und darüber hinaus funktioniert sie auch nach dem gleichen Prinzip: Der Konstruktor der Map erhält eine Liste von Entry-Datenstrukturen, die jeweils aus einem Key und einem Value bestehen, und aus denen sie sich selbst konstruiert. Aber keine Sorge: Natürlich ist das in C++ richtig verankert. Und insbesondere werden die Konstruktoren der Klasse für die Initialisierung ganz normal aufgerufen..
Ich habe bisher nur gezeigt, wir die neuen Initialisierungslisten auf Seite des Aufrufers aussehen. Die Bereitstellung dieser Funktionalität in eigenen Klassen ist relativ einfach. Im Detail werde ich aber erst in einem späteren Artikel darauf eingehen. Stattdessen zeige ich an dieser Stelle lediglich ein Beispiel. Ich hoffe damit zumindest ein Gefühl dafür vermitteln zu können.
#include <initializer_list>
template <typename Key, typename Value>
class map
{
class entry;
public:
// implicit constructor for brace initialization
map(std::initializer_list<entry> entries)
{
for (auto&& e : entries) {
(void) e; // NOTE: process e
}
}
};
template <typename Key, typename Value>
class map<Key, Value>::entry
{
Key _key;
Value _value;
public:
// implicit constructor for brace initialization
template <typename K, typename V>
entry(K&& key, V&& value)
: _key{std::forward<K>(key)}, _value{std::forward<V>(value)}
{ }
};
Das Beispiel soll zeigen, wie der Konstruktor der std::map
aussehen könnte, den ich weiter oben verwendet habe. C++ verwendet dafür keine spezielle Syntax, sondern eine magische
Template-Klasse aus der Standardbibliothek – nämlich std::initializer_list
. Und das ist bereits alles was dafür notwendig ist. Die Definition der Klasse map::entry
habe ich mit eingefügt, um nochmals deutlich zu machen, wie die Brace-Initialization mit Konstruktoren zusammenspielt..
Ich habe dieses neue Feature in der Standardbibliothek in ungefähr zwanzig Klassen-Templates gefunden. Daher gehe ich davon aus, dass es auch einigermaßen häufig angewendet werden wird. Nur optisch wird dieses Feature in neuem Code vermutlich kaum auffallen. Nicht nur, dass man dafür auf spezielle, syntaktische Symbole verzichtet hat – die Syntax beim Aufrufer und beim aufgerufenen Konstruktor unterscheidet sich in keiner Weise von der bisherigen Syntax. Und damit ereilt std::initializer_list
vermutlich ein undankbares Schicksal: Obwohl es eine wichtige Rolle in der Entwicklung mit C++ spielt, wird es nie die Anerkennung dafür finden.