вставки против установки против оператора[] в C++ карте


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

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

  2. есть ли необходимость добавления установки на стандартный? Есть ли что-то, что было невозможно раньше без него?

4 123

4 ответа:

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

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

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

первое различие между ними заключается в том, что operator[] должен быть в состоянии построить инициализированный по умолчанию стоимостью, и поэтому он непригоден для типов значений, которые не могут быть по умолчанию инициализированный. Второе различие между ними заключается в том, что происходит, когда уже есть элемент с данным ключом. Элемент insert функция не будет изменять состояние карты, но вместо этого возвращает итератор к элементу (и 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>) и передает его в

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

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

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

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

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

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

вот пример для демонстрации:

#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
}

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

следующий код может помочь вам понять "общую идею картины" как 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

обратите внимание, что:

  1. 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 объект как полностью пассивный. Как только вы поймете эту "большую идею картины", важно затем создать резервную копию и понять, как использовать этого посредника