C++11: emplace vs. insert

von Hubert Schmid vom 2012-05-27

In C++11 wurden fast alle Container der Standardbibliothek um emplace-Funktionen ergänzt. Diese Funktionen existieren in mehreren Ausprägungen, und sie sind eng verwandt mit der Familie der insert-Funktionen. So entsprechen beispielsweise emplace_front und emplace_back den Funktionen push_front und push_back, und emplace und emplace_hint entsprechen der Funktion insert.

Im Folgenden werfe ich einen genaueren Blick auf diese neuen Funktionen und versuche ihre Vor- und Nachteile herauszustellen. Dabei möchte ich eine Empfehlung formulieren, in welchen Fällen die insert- beziehungsweise die emplace-Funktionen verwendet werden sollten.

gleich

In vielen Fällen können die insert-Funktionen einfach durch die entsprechenden emplace-Funktionen ersetzt werden. Das zeige ich exemplarisch anhand std::list::push_back. Im folgenden Code-Fragment verhält sich die Funktion emplace_back praktisch genauso wie wenn stattdessen push_back verwendet worden wäre. Das heißt es werden die gleichen Operationen auf std::string in der gleichen Reihenfolge ausgeführt.

// declaration of function returning string auto get_name() -> std::string; void append_name(std::list<std::string>& names) { std::string name = get_name(); names.emplace_back(name); }

Daran ändert sich auch nichts, wenn statt dem L‑Value ein R‑Value wie in names.emplace_back(get_name()) verwendet wird. Denn in C++11 wurde die Funktion push_back zur Unterstützung der Move-Semantik passend überladen. Wichtig ist, dass in allen vier Fällen nicht das übergebene Argument im Container gespeichert wird, sondern ein neu konstruiertes Objekt, das durch Copy- oder Move-Konstruktion aus dem Argument erzeugt wird.

unterschiedlich

Darin liegt der wesentliche Unterschied zwischen push_back und emplace_back. Letztere kann mit Hilfe der Variadic Templates und des Perfect Forwarding fast jeden Konstruktor verwenden, um das Objekt im Container zu erzeugen. Die Funktion akzeptiert beliebige Argumente und reicht sie unverändert weiter. Das Verhalten kann man sich vereinfacht wie folgt vorstellen – nur ohne die Indirektion über ein temporäres Objekt:

names.emplace_back(expr1, expr2, ..., exprN); // ... is similar to ... but without temporary object names.push_back(std::string(expr1, expr2, ..., exprN));

Ein großer Vorteil von emplace_back ist schlicht und ergreifend die verbesserte Lesbarkeit, weil redundante und störende Information entfällt. Dazu kommt natürlich die bessere Performance. Diesen Punkt sollte man allerdings nicht überwerten. Denn für das temporäre Objekt greift in der Regel die Move-Semantik, die nur einen geringen Overhead verursacht. Das folgende Beispiel habe ich an anderer Stelle bereits verwendet und soll diese beiden Punkte nochmals motivieren:

auto split(const std::string& value, char separator) -> std::vector<std::string> { std::vector<std::string> result; std::string::size_type p = 0; std::string::size_type q; while ((q = value.find(separator, p)) != std::string::npos) { result.emplace_back(value, p, q - p); p = q + 1; } result.emplace_back(value, p); return result; }

In vielen Fällen lässt sich also die Funktion push_back einfach und ohne Nachteile durch emplace_back ersetzen. Es gibt aber ein paar Unterschiede, die man beachten sollte. Das ist im folgenden Beispiel zu sehen:

struct foobar { explicit foobar(int value); }; std::list<foobar> values; values.push_back(42); // ERROR values.emplace_back(42); // OK

Der Aufruf mit push_back führt zu einem Fehler bei der Übersetzung, da das Argument nicht implizit konvertiert werden kann. Der Aufruf mit emplace_back funktioniert hingegen, da das Objekt explizit aus dem Argumenten konstruiert wird. In einigen Fällen kann dieses Detail zu überraschenden Ergebnissen führen, wie in dem folgenden, konstruierten Beispiel:

struct foobar { foobar(double value); explicit foobar(int value); }; std::list<foobar> values; values.push_back(42); // invokes foobar(double) values.emplace_back(42); // invokes foobar(int)

In diesem Fall verwenden die beiden Funktionsaufrufe unterschiedliche Konstruktoren. Das ist irritierend, liegt aber in erster Linie an der schlecht entworfenen Klasse und weniger an den beiden Funktionen.

bedingt

In den bisher betrachteten Beispielen konnte emplace_back durchweg überzeugen. Das ist allerdings nicht immer so: Es gibt Fälle, in denen die entsprechende insert-Funktion überlegen ist. Ein solcher Fall ist im folgenden Beispiel zu sehen:

std::unordered_set<std::string> set; std::string line; while (getline(std::cin, line)) { set.emplace(line); }

Nochmals zur Wiederholung: Der wesentliche Unterschied zwischen den insert- und emplace-Funktionen ist, dass Erstere eine Kopie des Arguments im Container einfügen, wohingegen Letztere das einzufügende Element aus den Argumenten konstruieren. Im Falle einer std::unordered_set wird das Element allerdings nur eingefügt, wenn noch kein äquivalentes Element existiert. Die insert-Funktion prüft zuerst, ob das Element bereits existiert. Wenn es nicht existiert, wird die Kopie im Container erzeugt. Die emplace-Funktion muss hingegen zuerst das einzufügende Element konstruieren um feststellen zu können, ob es bereits existiert.

In diesem Beispiel ist also die emplace-Funktion signifikant langsamer als die insert-Funktion, wenn sich die Eingabezeilen hinreichend häufig wiederholen. Denn in diesem Fall werden viele unnötige std::string-Objekte erzeugt.

speziell

Es gibt noch zwei weitere Unterschiede, auf die ich eingehen möchte. Der Erste betrifft die Reihenfolge der Operationen. Dazu habe ich folgendes Code-Fragment:

std::list<std::thread> threads; auto&& worker = ...; threads.push_back(std::thread(worker));

Die letzte Zeile startet einen neuen Thread mit dem angegebenen Funktionsobjekt und fügt das Thread-Objekt anschließend in den Container ein. Der Code sieht ganz harmlos aus. Tatsächlich ist er aber sehr problematisch. Denn was passiert, wenn nicht genügend Speicher vorhanden ist, um das Thread-Objekt in den Container einzufügen? In diesem Fall wirft die Funktion push_back eine Ausnahme und das temporäre Thread-Objekt wird zerstört. Da weder join noch detach am Thread-Objekt aufgerufen wurden, wird das Programm mit std::terminate zwangsweise beendet. Der aufrufende Code hat keinerlei Möglichkeit das zu verhindern.

Verwendet man stattdessen die Funktion emplace_back, so wird zunächst der Speicher im Container reserviert und erst anschließend der Thread erzeugt. Oder umgekehrt: Wenn in der dritten Zeile eine Ausnahme auftritt, so wurde kein Thread erzeugt. Die Reihenfolge dieser beiden Operationen ist also entscheidend für eine robuste Implementierung.

std::list<std::thread> threads; auto&& worker = ...; threads.emplace_back(worker);

Der zweite Unterschied betrifft den Umgang mit Typen, die weder kopierbar noch verschiebbar sind. In der Standardbibliothek gibt es dafür kein Beispiel, da eine Move-Operation eigentlich immer implementiert werden kann. Aber in Legacy-Bibliotheken gibt es durchaus solche Klassen. Wie im folgenden Code zu sehen kann man mit emplace_back auch solche Typen in Container speichern. Das war bisher überhaupt nicht möglich.

struct nonmovable { nonmovable(int) { } nonmovable(const nonmovable&) = delete; auto operator=(const nonmovable&) & -> nonmovable& = delete; }; std::list<nonmovable> list; list.emplace_back(42);

abschließend

Insgesamt betrachtet bieten die emplace-Funktionen einige interessante Vorteile gegenüber den entsprechenden insert-Funktionen. Erstere kann man abseits von std::set und std::unordered_set bedenkenlos bevorzugen. Und bei diesen beiden Containern lohnt sich im Zweifelsfall ein genauer Blick.