Piecewise Construct
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.