Piecewise Construct

von Hubert Schmid vom 2012-07-01

Was macht das Objekt std::piecewise_construct? Auf diese Frage versuche ich eine einfache Antwort zu geben. Um am einfachsten geht das aus meiner Sicht mit Datentypen, die weder Copyable noch Movable sind.

Ein einfaches Beispiel für einen solchen Typ ist std::atomic<int>. Im folgenden Code-Auszug sind alle drei push_back-Operationen unzulässig, da unabhängig davon, wie das einzufügende Objekt erzeugt wird, das Objekt beim Einfügen stets kopiert werden muss.

std::list<std::atomic<int>> atomics; atomics.push_back(std::atomic<int>{ 42 }); // ERROR atomics.push_back({ 42 }); // ERROR atomics.push_back(42); // ERROR

Die Lösung ist im Fall von std::list einfach: Das Klassentemplate enthält die Funktion emplace_back, die wie push_back ein Element am Ende einfügt. Allerdings wird der Funktion nicht das einzufügende Element übergeben, sondern die Teile, aus denen das Objekt innerhalb des Containers erzeugt wird.

atomics.emplace_back(42); // OK

Interessant wird die Situation bei Objekten, die mit mehr als einem Argument konstruiert werden. Um das zu zeigen führe ich zunächst die beiden Beispielklassen person und email ein:

class person : noncopyable { std::string _name; int _age; public: person(std::string name, int age) : _name{std::move(name)} , _age{std::move(age)} { } }; class email : noncopyable { std::string _local_part; std::string _domain; public: email(std::string local_part, std::string domain) : _local_part{std::move(local_part)} , _domain{std::move(domain)} { } };

Die Funktion std::list::emplace_back unterstützt eine beliebige Anzahl Parameter. Genauso wie oben mit std::atomic<int> ist auch hier das Einfügen kein Problem:

std::list<person> persons; persons.emplace_back("Max Mustermann", 42); // OK std::list<email> emails; emails.emplace_back("max.mustermann", "example.com"); // OK

Der gleiche Trick funktioniert aber nicht bei einer Klasse wie std::map<email, person>. Denn dort müssen bereits auf oberster Ebene zwei Objekte übergeben werden, und die Initialisierung kann nicht einfach flachgedrückt werden, weil dabei die Struktur verloren ginge:

std::map<email, person> mapping; mapping.emplace("max.mustermann", "example.com", "Max Mustermann", 42); // ERROR

Jetzt kommt langsam das std::piecewise_construct ins Spiel. Die Idee besteht zunächst darin, die Initialisierungsargumente der beiden Objekte zusammenzufassen – beziehungsweise genauer – nur die Referenzen darauf. Dafür eignet sich die Klasse std::tuple aus der Standardbibliothek:

mapping.emplace( std::forward_as_tuple("max.mustermann", "example.com"), std::forward_as_tuple("Max Mustermann", 42));

Das funktioniert so noch nicht. Denn irgendwo müssen die beiden Tupel wieder in die einzelnen Argumente aufgeteilt werden. Damit nicht jede Klasse einen entsprechenden Konstruktor bereitstellen muss, steckt in der std::map ein wenig Magie (genauer in std::pair). Wird nämlich der emplace-Funktion als erstes Argument std::piecewise_construct übergeben, dann werden die restlichen Argumente aufgespalten.

mapping.emplace( std::piecewise_construct, std::forward_as_tuple("max.mustermann", "example.com"), std::forward_as_tuple("Max Mustermann", 42));

Dieser Code funktioniert: Die Objekte werden direkt aus ihren Argumenten in der std::map erzeugt – ohne die Argumente oder die erzeugten Objekte jemals zu kopieren oder zu verschieben. Das Ganze finde ich ziemlich abgefahren. Ich nehme an, dass man diese Funktionalität kaum sehen wird, weil es sich um einen sehr eingeschränkten Anwendungsfall handelt. Trotzdem finde ich interessant, was sich in C++ allein mit der Bibliothek alles bewerkstelligen lässt.