Вставить vs emplace vs operator [] в С++ map

Я использую карты в первый раз, и я понял, что есть много способов вставить элемент. Вы можете использовать emplace(), operator[] или insert(), плюс варианты, такие как использование value_type или make_pair. Хотя есть много информации обо всех них и вопросы о конкретных случаях, я все еще не могу понять общую картину. Итак, мои два вопроса:

  • В чем преимущество каждого из них над другими?

  • Была ли необходимость добавления emplace в стандарт? Есть ли что-то, что было невозможно раньше, без него?

Ответ 1

В частном случае карты старые параметры были только двумя: operator[] и insert (разные варианты insert). Поэтому я начну объяснять их.

operator[] - оператор find-or-add. Он попытается найти элемент с заданным ключом внутри карты, и если он существует, он вернет ссылку на сохраненное значение. Если это не так, он создаст новый элемент, вставленный на место с инициализацией по умолчанию, и вернет ссылку на него.

Функция insert (в аромате одного элемента) принимает value_type (std::pair<const Key,Value>), она использует ключ (first member) и пытается его вставить. Поскольку std::map не допускает дублирования, если есть существующий элемент, он ничего не вставляет.

Первое различие между ними состоит в том, что operator[] должно иметь возможность построить инициализированное значение по умолчанию, и поэтому оно непригодно для типов значений, которые не могут быть инициализированы по умолчанию. Второе различие между ними - это то, что происходит, когда уже есть элемент с данным ключом. Функция insert не будет изменять состояние карты, а вместо этого возвратит итератор в элемент (и a false, указывающий, что он не был вставлен).

// assume m is std::map<int,int> already has an element with key 5 and value 0
m[5] = 10;                      // postcondition: m[5] == 10
m.insert(std::make_pair(5,15)); // m[5] is still 10

В случае insert аргумент является объектом value_type, который может быть создан по-разному. Вы можете напрямую построить его с соответствующим типом или передать любой объект, из которого может быть сконструирован value_type, где находится std::make_pair, поскольку он позволяет просто создавать объекты std::pair, хотя, вероятно, это не так что вы хотите...

Чистый эффект следующих вызовов аналогичен:

K t; V u;
std::map<K,V> m;           // std::map<K,V>::value_type is std::pair<const K,V>

m.insert( std::pair<const K,V>(t,u) );      // 1
m.insert( std::map<K,V>::value_type(t,u) ); // 2
m.insert( std::make_pair(t,u) );            // 3

Но на самом деле это не то же самое... [1] и [2] фактически эквивалентны. В обоих случаях код создает временный объект того же типа (std::pair<const K,V>) и передает его функции insert. Функция insert создаст соответствующий node в двоичном дереве поиска, а затем скопирует часть value_type из аргумента в node. Преимущество использования value_type заключается в том, что, хорошо, value_type всегда соответствует value_type, вы не можете ошибаться в типе аргументов std::pair!

Различие в [3]. Функция std::make_pair является функцией шаблона, которая создаст std::pair. Подпись:

template <typename T, typename U>
std::pair<T,U> make_pair(T const & t, U const & u );

Я намеренно не предоставил аргументы шаблона std::make_pair, так как это обычное использование. И подразумевается, что аргументы шаблона выводятся из вызова, в данном случае для T==K,U==V, поэтому вызов std::make_pair будет возвращать std::pair<K,V> (обратите внимание на отсутствующий const). Для подписи требуется value_type, близкое, но не такое же, как возвращаемое значение от вызова до std::make_pair. Поскольку он достаточно близко, он создаст временный тип правильного типа и скопирует его. Это, в свою очередь, будет скопировано в node, создав в общей сложности две копии.

Это можно устранить, предоставив аргументы шаблона:

m.insert( std::make_pair<const K,V>(t,u) );  // 4

Но это все еще подвержено ошибкам таким же образом, что явно вводит тип в случае [1].

До этого момента у нас есть разные способы вызова insert, которые требуют создания value_type извне и копии этого объекта в контейнер. В качестве альтернативы вы можете использовать operator[], если тип по умолчанию конструктивен и назначен (намеренно фокусируется только на m[k]=v), и для этого требуется инициализация по умолчанию для одного объекта и копия значения в этот объект.

В С++ 11 с вариативными шаблонами и совершенной пересылкой есть новый способ добавления элементов в контейнер посредством размещения (создания на месте). Функции emplace в разных контейнерах делают в основном одно и то же: вместо получения источника, из которого следует копировать в контейнер, функция принимает параметры, которые будут перенаправлены конструктору объекта, хранящегося в контейнере.

m.emplace(t,u);               // 5

В [5] std::pair<const K, V> не создается и не передается в emplace, а ссылки на объекты t и u передаются в emplace, который перенаправляет их в конструктор value_type подобъект внутри структуры данных. В этом случае копии std::pair<const K,V> вообще не выполняются, что является преимуществом emplace по сравнению с альтернативами С++ 03. Как и в случае insert, он не будет переопределять значение на карте.


Интересный вопрос, о котором я не думал, заключается в том, как emplace можно реально реализовать для карты, и это не простая проблема в общем случае.

Ответ 2

Emplace: использует ссылку rvalue для использования фактических объектов, которые вы уже создали. Это означает, что не вызывается конструктор копирования или перемещения, подходит для объектов LARGE! O (log (N)).

Вставка: имеет перегрузки для стандартных ссылок на lvalue и rvalue, а также итераторы для списков элементов для вставки и "подсказок" относительно позиции, которой принадлежит элемент. Использование итератора "подсказки" может привести к тому, что временная вставка будет уменьшаться до конца времени, в противном случае это время O (log (N)).

Оператор []: проверяет, существует ли объект, и если он это делает, изменяет ссылку на этот объект, в противном случае использует предоставленный ключ и значение для вызова make_pair на двух объектах, а затем выполняет ту же работу, что и insert функция. Это время O (log (N)).

make_pair: немного больше, чем сделать пару.

Не было необходимости в добавлении стандарта к стандарту. В С++ 11 я считаю, что && тип ссылки был добавлен. Это устранило необходимость в семантике перемещения и позволило оптимизировать определенный тип управления памятью. В частности, ссылка rvalue. Оператор перегруженной вставки (value_type &) не использует семантику in_place и, следовательно, гораздо менее эффективен. Хотя он обеспечивает возможность обращения к rvalue-ссылкам, он игнорирует их ключевую цель, которая заключается в создании объектов.

Ответ 3

Помимо возможностей оптимизации и более простого синтаксиса, важное различие между вставкой и заменой заключается в том, что последнее позволяет осуществлять явные преобразования. (Это по всей стандартной библиотеке, а не только для карт.)

Вот пример, демонстрирующий:

#include <vector>

struct foo
{
    explicit foo(int);
};

int main()
{
    std::vector<foo> v;

    v.emplace(v.end(), 10);      // Works
    //v.insert(v.end(), 10);     // Error, not explicit
    v.insert(v.end(), foo(10));  // Also works
}

Это, по общему признанию, очень конкретная деталь, но когда вы имеете дело с целями пользовательских преобразований, стоит помнить об этом.

Ответ 4

Следующий код может помочь вам понять идею "большой картины" о том, как insert() отличается от emplace():

#include <iostream>
#include <unordered_map>
#include <utility>

struct Foo {
  static int foo_counter;
  int val;

  Foo() { val = foo_counter++;
    std::cout << "Foo() with val:                " << val << '\n';
  }
  Foo(int value) : val(value) { foo_counter++;
    std::cout << "Foo(int) with val:             " << val << '\n';
  }
  Foo(Foo& f2) { val = foo_counter++;
    std::cout << "Foo(Foo &) with val:           " << val
              << " \tcreated from:      \t" << f2.val << '\n';
  }
  Foo(const Foo& f2) { val = foo_counter++;
    std::cout << "Foo(const Foo &) with val:     " << val
              << " \tcreated from:      \t" << f2.val << '\n';
  }
  Foo(Foo&& f2) { val = foo_counter++;
    std::cout << "Foo(Foo&&) moving:             " << f2.val
              << " \tand changing it to:\t" << val << '\n';
  }
  ~Foo() { std::cout << "~Foo() destroying:             " << val << '\n'; }

  Foo& operator=(const Foo& rhs) {
    std::cout << "Foo& operator=(const Foo& rhs) with rhs.val: " << rhs.val
              << " \tcalled with lhs.val = \t" << val
              << " \tChanging lhs.val to: \t" << rhs.val << '\n';
    val = rhs.val;
    return *this;
  }

  bool operator==(const Foo &rhs) const { return val == rhs.val; }
  bool operator<(const Foo &rhs)  const { return val < rhs.val;  }
};

int Foo::foo_counter = 0;

//Create a hash function for Foo in order to use Foo with unordered_map
namespace std {
   template<> struct hash<Foo> {
       std::size_t operator()(const Foo &f) const {
           return std::hash<int>{}(f.val);
       }
   };
}

int main()
{
    std::unordered_map<Foo, int> umap;  
    Foo foo0, foo1, foo2, foo3;
    int d;

    std::cout << "\numap.insert(std::pair<Foo, int>(foo0, d))\n";
    umap.insert(std::pair<Foo, int>(foo0, d));
    //equiv. to: umap.insert(std::make_pair(foo0, d));

    std::cout << "\numap.insert(std::move(std::pair<Foo, int>(foo1, d)))\n";
    umap.insert(std::move(std::pair<Foo, int>(foo1, d)));
    //equiv. to: umap.insert(std::make_pair(foo1, d));

    std::cout << "\nstd::pair<Foo, int> pair(foo2, d)\n";
    std::pair<Foo, int> pair(foo2, d);

    std::cout << "\numap.insert(pair)\n";
    umap.insert(pair);

    std::cout << "\numap.emplace(foo3, d)\n";
    umap.emplace(foo3, d);

    std::cout << "\numap.emplace(11, d)\n";
    umap.emplace(11, d);

    std::cout << "\numap.insert({12, d})\n";
    umap.insert({12, d});

    std::cout.flush();
}

Выход, который я получил, был:

Foo() with val:                0
Foo() with val:                1
Foo() with val:                2
Foo() with val:                3

umap.insert(std::pair<Foo, int>(foo0, d))
Foo(Foo &) with val:           4    created from:       0
Foo(Foo&&) moving:             4    and changing it to: 5
~Foo() destroying:             4

umap.insert(std::move(std::pair<Foo, int>(foo1, d)))
Foo(Foo &) with val:           6    created from:       1
Foo(Foo&&) moving:             6    and changing it to: 7
~Foo() destroying:             6

std::pair<Foo, int> pair(foo2, d)
Foo(Foo &) with val:           8    created from:       2

umap.insert(pair)
Foo(const Foo &) with val:     9    created from:       8

umap.emplace(foo3, d)
Foo(Foo &) with val:           10   created from:       3

umap.emplace(11, d)
Foo(int) with val:             11

umap.insert({12, d})
Foo(int) with val:             12
Foo(const Foo &) with val:     13   created from:       12
~Foo() destroying:             12

~Foo() destroying:             8
~Foo() destroying:             3
~Foo() destroying:             2
~Foo() destroying:             1
~Foo() destroying:             0
~Foo() destroying:             13
~Foo() destroying:             11
~Foo() destroying:             5
~Foo() destroying:             10
~Foo() destroying:             7
~Foo() destroying:             9

Обратите внимание:

  • An unordered_map всегда хранит Foo объекты (а не, скажем, Foo * s) как ключи, которые уничтожаются при уничтожении unordered_map. Здесь внутренними ключами unordered_map были foos 13, 11, 5, 10, 7 и 9.

    • Таким образом, наш unordered_map на самом деле хранит объекты std::pair<const Foo, int>, которые, в свою очередь, хранят объекты Foo. Но чтобы понять идею "большой картины" о том, как emplace() отличается от insert() (см. Раздел ниже), можно временно представить этот объект std::pair как полностью пассивный. Как только вы поймете эту идею "большой картины", важно вернуться и понять, как использование этого промежуточного объекта std::pair unordered_map представляет тонкие, но важные технические проблемы.
  • Вставка каждого из foo0, foo1 и foo2 требовала 2 вызова одного из конструкторов Foo copy/move и 2 вызовов Foo деструктора (как я теперь описываю):

    а. Вставка каждого из foo0 и foo1 создала временный объект (foo4 и foo6, соответственно), деструктор которого был немедленно вызван после завершения вставки. Кроме того, unordered_map internal Foo (которые являются foos 5 и 7) также имеют свои деструкторы, вызываемые при уничтожении unordered_map.

    б. Чтобы вставить foo2, вместо этого мы сначала создали невременную пару (foo8), которая вызвала конструктор Foo copy, а затем вставила его, что привело к тому, что unordered_map снова вызвал конструктор копирования для создания внутренней копии (foo9). Как и в случае с foos 0 и 1, конечным результатом было два вызова деструктора для этой вставки с той лишь разницей, что деструктор foo8 вызывался только тогда, когда мы достигли конца main().

  • Внедрение foo3 привело только к 1 вызову конструктора копирования/перемещения (создание foo10 внутри) и только 1 вызов Foo деструктора. (Я вернусь к этому позже).

  • Для foo11 мы прямо передали целое число 11, чтобы unordered_map вызывал конструктор Foo(int). В отличие от (3), для этого нам даже не понадобился какой-либо предварительный объект foo.

Это показывает, какова главная разница "большой картины" между insert() и emplace():

В то время как использование insert() почти всегда требует построения или существования некоторого объекта Foo в области main() (за которым следует копия или перемещение), при использовании emplace() тогда любой вызов конструктора выполняется полностью внутри в unordered_map (т.е. внутри области определения метода emplace()). Аргумент для ключа, который вы передаете в emplace(), напрямую перенаправляется на вызов конструктора Foo внутри unordered_map (необязательно более подробная информация: где этот вновь построенный объект немедленно включается в одну из переменных unordered_map так что никакой деструктор не вызывается, когда выполнение оставляет emplace() и не вызывается никаких конструкторов перемещения или копирования).

Примечание: причина почти почти всегда объясняется в I) ниже.

  1. продолжение: причина, по которой вызов umap.emplace(foo3, d) называется конструктором Foo non-const copy выглядит следующим образом: поскольку мы используем emplace(), компилятор знает, что foo3 (объект non-const Foo) означает аргумент для некоторого конструктора Foo. В этом случае наиболее подходящим конструктором Foo является неконстантная копия конструкции Foo(Foo& f2). Вот почему umap.emplace(foo3, d) называется конструктором копирования, а umap.emplace(11, d) - нет.

Эпилог:

я. Обратите внимание, что одна перегрузка insert() фактически эквивалентна emplace(). Как описано на этой странице cppreference.com, перегрузка template<class P> std::pair<iterator, bool> insert(P&& value) (которая является перегрузкой (2) insert() на этом cppreference.com страница) эквивалентна emplace(std::forward<P>(value)).

II. Куда пойти отсюда?

а. Играйте со следующим исходным кодом и учебной документацией для insert() (например, здесь) и emplace() (например, здесь), который найден в Интернете. Если вы используете среду IDE, такую ​​как eclipse или NetBeans, вы можете легко заставить вашу среду IDE указать вам, какая перегрузка insert() или emplace() вызывается (в eclipse просто держите курсор мыши устойчивым по вызову функции для Второй). Вот еще один код для тестирования:

std::cout << "\numap.insert({{" << Foo::foo_counter << ", d}})\n";
umap.insert({{Foo::foo_counter, d}});
//but umap.emplace({{Foo::foo_counter, d}}); results in a compile error!

std::cout << "\numap.insert(std::pair<const Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<const Foo, int>({Foo::foo_counter, d}));
//The above uses Foo(int) and then Foo(const Foo &), as expected. but the
// below call uses Foo(int) and the move constructor Foo(Foo&&). 
//Do you see why?
std::cout << "\numap.insert(std::pair<Foo, int>({" << Foo::foo_counter << ", d}))\n";
umap.insert(std::pair<Foo, int>({Foo::foo_counter, d}));
//Not only that, but even more interesting is how the call below uses all 
// three of Foo(int) and the Foo(Foo&&) move and Foo(const Foo &) copy 
// constructors, despite the below call only difference to the call above 
// being the additional { }.
std::cout << "\numap.insert({std::pair<Foo, int>({" << Foo::foo_counter << ", d})})\n";
umap.insert({std::pair<Foo, int>({Foo::foo_counter, d})});


//Pay close attention to the subtle difference in the effects of the next 
// two calls.
int cur_foo_counter = Foo::foo_counter;
std::cout << "\numap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}}) where " 
  << "cur_foo_counter = " << cur_foo_counter << "\n";
umap.insert({{cur_foo_counter, d}, {cur_foo_counter+1, d}});

std::cout << "\numap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}}) where "
  << "Foo::foo_counter = " << Foo::foo_counter << "\n";
umap.insert({{Foo::foo_counter, d}, {Foo::foo_counter+1, d}});


//umap.insert(std::initializer_list<std::pair<Foo, int>>({{Foo::foo_counter, d}}));
//The call below works fine, but the commented out line above gives a 
// compiler error. It instructive to find out why. The two cals
// differ by a "const".
std::cout << "\numap.insert(std::initializer_list<std::pair<const Foo, int>>({{" << Foo::foo_counter << ", d}}))\n";
umap.insert(std::initializer_list<std::pair<const Foo, int>>({{Foo::foo_counter, d}}));

Скоро вы увидите, какая перегрузка конструктора std::pair (см. reference), который используется unordered_map, может существенно повлиять на то, сколько объектов копируется, перемещается, создается и/или уничтожается, и когда это происходит.

б. Посмотрите, что произойдет, если вы используете другой класс контейнера (например, std::set или std::unordered_multiset) вместо std::unordered_map.

с. Теперь используйте объект Goo (только переименованную копию Foo) вместо int как тип диапазона в unordered_map (т.е. Используйте unordered_map<Foo, Goo> вместо unordered_map<Foo, int>) и посмотрите, сколько и какие Вызываются конструкторы Goo. (Спойлер: есть эффект, но это не очень драматично.)