std::map: emplace und die kontrollierte Objekterzeugung

von Hubert Schmid vom 2014-04-20

Seit C++11 besitzen viele Container der Standardbibliothek eine oder mehrere emplace-Funktionen, mit denen sich Objekte an Ort und Stelle erzeugen lassen. Auch std::map bietet mit emplace und emplace_hint zwei solcher Funktionen. In diesem Artikel betrachte ich, wie sich unnötige Objekterzeugungen beim Einfügen von Elementen in eine std::map vermeiden lassen, und welche Rolle emplace dabei spielt.

Zunächst einmal ist ein wichtiger Use-Case zu nennen: Möchte man Objekte direkt in eine std::map einfügen, die weder kopierbar noch verschiebbar sind, dann funktioniert das nur über eine der beiden emplace-Funktionen. Das folgende Listing zeigt ein Beispiel mit std::atomic. Für die erste Anweisung mit insert liefert der Compiler einen Fehler, da std::atomic<int> nicht kopierbar ist. Die Anweisung mit emplace funktioniert dagegen.

std::map<std::string, std::atomic<int>> counters; counters.insert({ "foo", 0 }); // ERROR: call to implicitly-deleted constructor counters.emplace(std::piecewise_construct, std::forward_as_tuple("foo"), std::forward_as_tuple());

Der emplace-Aufruf sieht ein wenig umständlich aus. Leider ist in diesem Fall die einfache Form emplace("foo", 0) aufgrund einer übermäßig restriktiven Spezifikation nicht möglich. Dafür kann diese Form auch verwendet werden, wenn die Objekte mit mehr als einem Argument konstruiert werden sollen.

Der Aufruf von emplace sollte allerdings nicht darüber hinwegtäuschen, dass das Objekt selbst dann erzeugt wird, wenn bereits ein Eintrag mit gleichem Key existiert. Die Formulierung im Sprachstandard ist diesbezüglich etwas schwammig. Doch bei genauerer Betrachtung wird offensichtlich, dass die Konstruktion notwendig ist, um feststellen zu können, ob dies der Fall ist.

Im Fall von std::atomic spielt die unnötige Konstruktion keine Rolle. In anderen Fällen kann es jedoch sehr wohl ein Problem sein – entweder weil die Konstruktion aufwendig ist, oder weil sie hinderlich ist. Möchte man diesen Fall vermeiden, so bleibt einem nichts anderes übrig, als vorab zu testen, ob ein entsprechender Key bereits existiert. Dafür bietet sich die Member-Funktion lower_bound an, deren Rückgabewert mit emplace_hint verwendet werden kann, um eine zweite Suche zu vermeiden.

std::map<std::string, std::string> tokens; auto p = tokens.lower_bound("foo"); if (p == tokens.end() || p->first != "foo") { tokens.emplace_hint(p, "foo", "bar"); }

In diesem Fall wird zwar aus "bar" nur dann ein std::string erzeugt, wenn tatsächlich ein Eintrag eingefügt wird. Doch der Code hat das Problem unnötiger Konstruktionen nicht gelöst sondern nur an eine andere Stelle verschoben: Beim Aufruf von lower_bound wird aus "foo" ein std::string für die Suche erzeugt.

Mit C++14 gibt es dafür allerdings eine Lösung: Ab dieser Version können die Member-Funktionen find, lower_bound und upper_bound mit beliebigen Typen verwendet werden – vorausgesetzt sie werden von der Vergleichsfunktion unterstützt, die der Datenstruktur zu Grund liegt. Das folgende Listing enthält dazu ein Beispiel. Die Klasse transparent_less unterstützt neben dem Vergleich zweier std::string-Objekte auch alle anderen Typen, die von operator< unterstützt werden. Signalisiert wird diese Unterstützung durch den Member is_transparent.

struct transparent_less { using is_transparent = void; template <typename Lhs, typename Rhs> bool operator()(Lhs&& lhs, Rhs&& rhs) const { return lhs < rhs; } };

Der Typ transparent_less dient nur zur Veranschaulichung, denn mit der Spezialisierung std::less<> gibt es in der Standardbibliothek bereits einen Typ, der sich genau so verhält. Dieser kommt im folgenden Listing zum Einsatz. Der restliche Code ist identisch zum obigen Listing. Doch dieses Mal werden die std::string-Objekte nur noch erzeugt, wenn tatsächlich ein Element in die std::map eingefügt wird.

std::map<std::string, std::string, std::less<>> tokens; auto p = tokens.lower_bound("foo"); if (p == tokens.end() || p->first != "foo") { tokens.emplace_hint(p, "foo", "bar"); }

Zu dem Beispiel sollte man anmerken, dass die Implementierung dadurch nicht zwangsweise effizienter wird. Es wird zwar kein unnötiges std::string-Objekt mehr erzeugt. Dafür ist der Vergleich zweier Zeichenketten jedoch potentiell teurer, da die Länge a priori nicht bekannt ist. Unabhängig davon kann man festhalten, dass C++14 alle Voraussetzungen enthält, um unnötige Objekt-Erzeugungen zu unterbinden, dass die Member-Funktion emplace dabei allerdings nur eine kleine Rolle spielt.