Jste zde

Potřeba defaultního konstruktoru při vkládání do std::map

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á.

Úvodní příklad

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?

Proč to chce defaultní konstruktor?

Pes je zakopaný v tom, že m[1] ve skutečnosti dělá následující:

  1. Podívá se, zda v mapě existuje osoba s ID 1.
  2. Pokud existuje, tak se na ní vrátí reference.
  3. Pokud neexistuje, tak se vytvoří nová osoba přes defaultní konstruktor, přiřadí se k ID 1 a vrátí se na ni reference.

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?

Lze se vyhnout potřebě mít defaultní konstruktor?

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:

  • insert() - Pokud daný klíč v mapě neexistuje, vloží na jeho místo daný prvek. U nás by to vypadalo takto:
    // 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.

  • emplace() - Novinka od C++11, která vloží nový prvek do mapy tak, že jej vytvoří "na místě". Oním prvkem se myslí 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.

Lze se vyhnout i potřebě mít copy/move konstruktor?

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.

Rozdíly mezi insert() a emplace()

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.

Zdrojáky

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ář