V příspěvku se podíváme na to, proč v C++ při vkládání objektů do std::map
přes operator[]
vyvstává potřeba mít definovaný defaultní konstruktor. Následně si ukážeme způsoby vkládání objektů, při kterých tato potřeba odpadá.
Mějme třídu, která nám reprezentuje osoby:
#include <map> #include <string> class Person { public: Person(const std::string &name, int age): name(name), age(age) {} // ... private: std::string name; int age; };
Dále mějme std::map
, která nám mapuje interní ID (int
) na tyto osoby:
std::map<int, Person> m;
Když se pokusíme do mapy vložit novou osobu přes operator[]:
m[1] = Person("Fred Astaire", 88);
dostaneme následující chybové hlášení (GCC 4.9):
[..]/tuple:1104:70: error: no matching function for call to ‘Person::Person()’
Vyvstává následující otázka: Proč to chce defaultní konstruktor, když přece voláme náš konstruktor, který požaduje jméno a věk?
Pes je zakopaný v tom, že m[1]
ve skutečnosti dělá následující:
Onen náš řádek s vložením do mapy se tedy pokusí vytvořit "prázdnou" osobu, vrátit na ní referenci a následně do ní přiřadit Freda. Jelikož jsme si definovali vlastní konstruktor, tak překladač za nás defaultní konstruktor nevytvoří a zahlásí chybu.
Funguje to tak z toho důvodu, že operator[]
vrací referenci na položku na daném klíči a jelikož v C++ neexistují null reference, je potřeba něco vrátit. Pokud byste chtěli, aby v takovém případě došlo k vyhození výjimky, lze použít at()
.
Tak či onak, ona "prázdná" osoba se tedy vytvoří úplně zbytečně. To nás přivádí na otázku: nešlo by se vytváření té "prázdné" osoby vyhnout?
Ano. Jde o to, že operator[]
není primárně určen pro vkládání do mapy, i když se k tomu často využívá (či zneužívá, podle toho, jak se na to díváte). Místo něj lze použít jednu z následujících metod:
// C++98 m.insert(std::map<int, Person>::value_type(1, Person("Fred Astaire", 88))); // nebo m.insert(std::make_pair(1, Person("Fred Astaire", 88))); // C++11 m.insert({1, Person("Fred Astaire", 88)});
Nyní už defaultní konstruktor není nutné mít a překlad projde. Co je nyní však potřeba, tak je přítomnost copy konstruktoru nebo move konstruktoru, abychom Freda dostali do mapy. Jelikož jej za nás automaticky vytvoří překladač, tak si to ani neuvědomíme, protože překlad projde. Pokud jej však explicitně zakážeme
Person(const Person &) = delete; // nebo Person(Person &&) = delete;
tak překlad skončí s chybou. Vyměnili jsme tedy potřebu defaultního konstruktoru za copy/move konstruktor.
std::pair
, což je value_type
pro std::map
(viz příklad s insert()
výše). U nás by to vypadalo takto:// C++11 m.emplace(1, Person("Fred Astaire", 88));
Není tak potřeba explicitně vytvářet std::pair
. Stále je však potřeba mít copy/move konstruktor pro Freda, o čemž se můžeme přesvědčit stejným způsobem, jako u insert()
výše.
Jak kdy. V našem případě, kdy vytváříme novou osobu, to možné lze, a to přes emplace()
volaný takto:
// C++11 m.emplace( std::piecewise_construct, std::forward_as_tuple(1), std::forward_as_tuple("Fred Astaire", 88) );
Využije se tam speciální konstruktor std::pair
(číslo 6 v odkazované stránce), který lze použít pro vytvoření std::pair
nekopírovatelných a nepřesouvatelných objektů. Ve výsledku to znamená, že se i Fred vytvoří přímo "na místě" a není tak potřeba žádná zbytečná kopie.
Pokud by vás zajímaly detaily, např. zda je emplace()
vždy rychlejší, než insert()
a zda jsou mezi nimi i další rozdíly, určitě doporučuji si přečíst položku 42 z knihy Effective Modern C++ či mrknout se na tuto přednášku, jejíž první část koresponduje k oné položce 42 ze Scottovy knihy.
Pokud byste si chtěli s příkladem pohrát, tak přeložitelné zdrojáky ke všem variantám včetně Makefile jsem hodil k sobě na GitHub.
Přidat komentář