inserir vs substituir vs operador [] no mapa c ++

192

Estou usando mapas pela primeira vez e percebi que existem várias maneiras de inserir um elemento. Você pode usar emplace(), operator[]ou insert(), mais variantes, como usar value_typeou make_pair. Embora exista muita informação sobre todos eles e perguntas sobre casos específicos, ainda não consigo entender o quadro geral. Então, minhas duas perguntas são:

  1. Qual é a vantagem de cada um deles em relação aos outros?

  2. Havia necessidade de adicionar lugar ao padrão? Existe algo que antes não era possível sem ele?

Capuano alemão
fonte
1
A semântica de colocação permite conversões explícitas e inicialização direta.
Kerrek SB
3
Agora operator[]é baseado try_emplace. Também vale a pena mencionar insert_or_assign.
FrankHB
@FrankHB se você (ou outra pessoa) adicionar uma resposta atualizada, eu poderia mudar a resposta aceita.
German Capuano

Respostas:

229

No caso particular de um mapa, as opções antigas eram apenas duas: operator[]e insert(sabores diferentes de insert). Então eu vou começar a explicar isso.

O operator[]é um operador de localização ou adição . Ele tentará encontrar um elemento com a chave especificada dentro do mapa e, se existir, retornará uma referência ao valor armazenado. Caso contrário, ele criará um novo elemento inserido no local com a inicialização padrão e retornará uma referência a ele.

A insertfunção (no tipo de elemento único) pega um value_type( std::pair<const Key,Value>), usa a chave ( firstmembro) e tenta inseri-la. Como std::mapnão permite duplicatas, se houver um elemento existente, ele não inserirá nada.

A primeira diferença entre os dois é que operator[]precisa ser capaz de construir um valor inicial padrão e, portanto, é inutilizável para tipos de valor que não podem ser inicializados por padrão. A segunda diferença entre os dois é o que acontece quando já existe um elemento com a chave fornecida. A insertfunção não modifica o estado do mapa, mas retorna um iterador para o elemento (e um falseindicando que não foi inserido).

// 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

No caso do insertargumento, é um objeto de value_type, que pode ser criado de diferentes maneiras. Você pode construí-lo diretamente com o tipo apropriado ou passar qualquer objeto a partir do qual ele value_typepossa ser construído, e é aí que std::make_pairentra em cena, pois permite a criação simples de std::pairobjetos, embora provavelmente não seja o que você deseja ...

O efeito líquido das seguintes chamadas é semelhante :

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

Mas não são realmente iguais ... [1] e [2] são realmente equivalentes. Nos dois casos, o código cria um objeto temporário do mesmo tipo ( std::pair<const K,V>) e o passa para a insertfunção. A insertfunção criará o nó apropriado na árvore de pesquisa binária e copiará a value_typeparte do argumento para o nó. A vantagem de usar value_typeé que, bem, value_typesempre corresponde value_type , você não pode digitar errado o tipo dos std::pairargumentos!

A diferença está em [3]. A função std::make_pairé uma função de modelo que criará um std::pair. A assinatura é:

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

Não forneci intencionalmente os argumentos do modelo std::make_pair, pois esse é o uso comum. E a implicação é que os argumentos do modelo são deduzidos da chamada, nesse caso, para que seja T==K,U==V, portanto a chamada para std::make_pairretornará a std::pair<K,V>(observe a falta const). A assinatura requer value_typeque seja próximo, mas não o mesmo que o valor retornado da chamada para std::make_pair. Por estar próximo o suficiente, ele criará um temporário do tipo correto e a cópia inicializará. Isso, por sua vez, será copiado para o nó, criando um total de duas cópias.

Isso pode ser corrigido fornecendo os argumentos do modelo:

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

Mas isso ainda é propenso a erros da mesma maneira que digitar explicitamente o tipo no caso [1].

Até esse ponto, temos diferentes maneiras de chamar insertque exigem a criação value_typeexterna e a cópia desse objeto no contêiner. Como alternativa, você pode usar operator[]se o tipo é padrão construtível e atribuível (focalizando intencionalmente apenas em m[k]=v) e requer a inicialização padrão de um objeto e a cópia do valor nesse objeto.

Em C ++ 11, com modelos variádicos e encaminhamento perfeito há uma nova maneira de adicionar elementos dentro de um recipiente por meio de emplacing (criando no lugar). As emplacefunções nos diferentes contêineres fazem basicamente a mesma coisa: em vez de obter uma fonte da qual copiar no contêiner, a função usa os parâmetros que serão encaminhados ao construtor do objeto armazenado no contêiner.

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

Em [5], o std::pair<const K, V>não é criado e passado para emplace, mas as referências ao objeto te usão passadas para emplaceque os encaminha ao construtor do value_typesubobjeto dentro da estrutura de dados. Nesse caso, nenhuma cópia std::pair<const K,V>é feita, o que é a vantagem das emplacealternativas C ++ 03. Como no caso insert, não substituirá o valor no mapa.


Uma pergunta interessante sobre a qual eu não havia pensado é como emplacerealmente pode ser implementado para um mapa, e esse não é um problema simples no caso geral.

David Rodríguez - dribeas
fonte
5
Isso é sugerido na resposta, mas map [] = val substituirá o valor anterior, se houver.
dk123
Uma pergunta mais interessante, no meu sentido, é que isso serve a pouco propósito. Como você salva a cópia em par, o que é bom porque nenhuma cópia em par significa que não há mapped_typecópia de segurança. O que queremos é substituir a construção do mapped_typeno par e substituir a construção do par no mapa. Portanto, a std::pair::emplacefunção e seu suporte de encaminhamento map::emplaceestão ausentes. Em sua forma atual, você ainda precisa fornecer um mapped_type construído para o construtor de pares que o copiará uma vez. é melhor que duas vezes, mas ainda não é bom.
precisa saber é
na verdade, eu altero esse comentário, no C ++ 11 existe um construtor de pares de modelos que serve exatamente ao mesmo propósito que substituir no caso de uma construção de argumento. e alguma construção estranha por partes, como a chamam, usando tuplas para encaminhar argumentos, para que ainda possamos ter o encaminhamento perfeito.
precisa saber é
Parece que há um erro de desempenho de inserção em unordered_map e map: link
Deqing
1
Pode ser bom atualizar isso com informações sobre insert_or_assigne try_emplace(ambas do C ++ 17), que ajudam a preencher algumas lacunas na funcionalidade dos métodos existentes.
ShadowRanger
15

Substituir: tira proveito da referência rvalue para usar os objetos reais que você já criou. Isso significa que nenhum construtor de copiar ou mover é chamado, bom para objetos GRANDES! O (log (N)) tempo.

Inserir: possui sobrecargas para a referência lvalue padrão e referência rvalue, além de iteradores para listas de elementos a serem inseridos e "dicas" sobre a posição em que um elemento pertence. O uso de um iterador de "dica" pode reduzir o tempo de inserção até o tempo contante; caso contrário, é o tempo O (log (N)).

Operador []: verifica se o objeto existe e, se existir, modifica a referência a esse objeto, usa a chave e o valor fornecidos para chamar make_pair nos dois objetos e, em seguida, faz o mesmo trabalho que a função de inserção. Este é o horário O (log (N)).

make_pair: Faz pouco mais do que formar um par.

Não havia "necessidade" de adicionar lugar ao padrão. Em c ++ 11, acredito que o tipo de referência && foi adicionado. Isso removeu a necessidade de semântica de movimentação e permitiu a otimização de algum tipo específico de gerenciamento de memória. Em particular, a referência rvalue. O operador de inserção sobrecarregada (value_type &&) não tira proveito da semântica in_place e, portanto, é muito menos eficiente. Embora ofereça a capacidade de lidar com referências a rvalues, ignora seu objetivo principal, que é a construção de objetos.

ChrisCM
fonte
4
" Não havia" necessidade "de adicionar lugar ao padrão." Isso é evidentemente falso. emplace()é simplesmente a única maneira de inserir um elemento que não pode ser copiado ou movido. (e sim, talvez, para inserir com mais eficiência um cujos construtores de copiar e mover custam muito mais do que a construção, se isso existir) Parece também que você entendeu errado: não se trata de " tirar proveito da referência rvalue" usar os objetos reais que você já criou "; nenhum objeto é criado ainda, e você encaminha os mapargumentos que necessita para criá-lo dentro de si. Você não faz o objeto.
underscore_d
10

Além das oportunidades de otimização e da sintaxe mais simples, uma distinção importante entre inserção e colocação é que esta permite conversões explícitas . (Isso ocorre em toda a biblioteca padrão, não apenas nos mapas.)

Aqui está um exemplo para demonstrar:

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

É um detalhe muito específico, mas quando você lida com cadeias de conversões definidas pelo usuário, vale a pena lembrar disso.

Kerrek SB
fonte
Imagine que foo exigisse duas entradas no seu ctor em vez de uma. Você seria capaz de usar esta chamada? v.emplace(v.end(), 10, 10); ... ou agora você precisaria usar v.emplace(v.end(), foo(10, 10) ); :?
quer
Não tenho acesso a um compilador no momento, mas assumirei que isso significa que ambas as versões funcionarão. Quase todos os exemplos que você vê emplaceusam uma classe que usa um único parâmetro. Na OMI, na verdade, tornaria a natureza da sintaxe variadica do lugar muito mais clara se vários parâmetros fossem usados ​​em exemplos.
Kaitain
9

O código a seguir pode ajudar você a entender a "ideia geral" de como insert()difere emplace():

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

//Foo simply outputs what constructor is called with what value.
struct Foo {
  static int foo_counter; //Track how many Foo objects have been created.
  int val; //This Foo object was the val-th Foo object to be created.

  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;

    //Print the statement to be executed and then execute it.

    std::cout << "\numap.insert(std::pair<Foo, int>(foo0, d))\n";
    umap.insert(std::pair<Foo, int>(foo0, d));
    //Side note: 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)));
    //Side note: 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();
}

A saída que obtive foi:

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

Notar que:

  1. Um unordered_mapsempre armazena internamente Fooobjetos (e não, digamos, Foo *s) como chaves, que são todos destruídos quando o objeto unordered_mapé destruído. Aqui, as unordered_mapchaves internas da empresa eram foos 13, 11, 5, 10, 7 e 9.

    • Então, tecnicamente, nosso unordered_maprealmente armazena std::pair<const Foo, int>objetos, que por sua vez, armazenam os Fooobjetos. Mas, para entender a "ideia geral" de como emplace()difere insert()(veja a caixa destacada abaixo), não há problema em imaginar temporariamente esse std::pairobjeto como sendo inteiramente passivo. Depois de entender essa "ideia geral", é importante fazer backup e entender como o uso desse std::pairobjeto intermediário unordered_mapintroduz sutis, mas importantes, aspectos técnicos.
  2. Inserindo cada uma foo0, foo1e foo2necessário 2 chamadas para um dos Fooconstrutores de copiar / mover s e 2 chamadas para Foo's destructor (como eu agora descrever):

    uma. A inserção de cada um foo0e foo1criou um objeto temporário ( foo4e foo6, respectivamente) cujo destruidor foi chamado imediatamente após a conclusão da inserção. Além disso, os Foos internos do unordered_map (que são Foos 5 e 7) também tiveram seus destruidores chamados quando o unordered_map foi destruído.

    b. Para inserir foo2, primeiro criamos explicitamente um objeto de par não temporário (chamado pair), que chamou Fooo construtor de cópias on foo2(criando foo8como um membro interno de pair). Em seguida, insert()editamos esse par, o que resultou em unordered_mapchamar o construtor de cópia novamente (on foo8) para criar sua própria cópia interna ( foo9). Assim como foos 0 e 1, o resultado final foram duas chamadas de destruidor para essa inserção, com a única diferença foo8: o destruidor foi chamado apenas quando chegamos ao final do main()que em ser chamado imediatamente após o insert()término.

  3. A colocação foo3resultou em apenas 1 chamada de copiar / mover do construtor (criando foo10internamente no unordered_map) e apenas 1 chamada no Foodestruidor. (Voltarei a isso mais tarde).

  4. Pois foo11, passamos diretamente o número inteiro 11 para emplace(11, d)que unordered_mapchamaria o Foo(int)construtor enquanto a execução estivesse dentro de seu emplace()método. Ao contrário de (2) e (3), nem precisávamos de um fooobjeto pré-existente para fazer isso. Importante, observe que Fooocorreu apenas 1 chamada para um construtor (que foi criado foo11).

  5. Passamos diretamente o número inteiro 12 para insert({12, d}). Diferentemente de emplace(11, d)(que a retirada resultou em apenas 1 chamada para um Fooconstrutor), essa chamada insert({12, d})resultou em duas chamadas para Fooo construtor (criando foo12e foo13).

Isso mostra qual é a principal diferença "grande" entre insert()e emplace()é:

Enquanto o uso insert() quase sempre requer a construção ou existência de algum Fooobjeto no main()escopo do (seguido de uma cópia ou movimentação), se o uso for feito emplace(), qualquer chamada a um Fooconstrutor será feita inteiramente internamente no unordered_map(ou seja, dentro do escopo da emplace()definição do método). Os argumentos da chave para a qual você passa emplace()são diretamente encaminhados para uma Foochamada de construtor dentro unordered_map::emplace()da definição (detalhes adicionais opcionais: onde esse objeto recém-construído é imediatamente incorporado a uma das unordered_mapvariáveis ​​de membro de modo que nenhum destruidor seja chamado quando execução deixa emplace()e nenhum movimento ou copiar construtores são chamados).

Nota: O motivo do " quase " em " quase sempre " acima é explicado em I) abaixo.

  1. continuação: A razão pela qual chamar o construtor de cópia não const umap.emplace(foo3, d)chamado Fooé o seguinte: Como estamos usando emplace(), o compilador sabe que foo3(um Fooobjeto não const ) deve ser um argumento para algum Fooconstrutor. Nesse caso, o Fooconstrutor mais adequado é o construtor de cópia não const Foo(Foo& f2). É por isso que umap.emplace(foo3, d)chamou um construtor de cópias enquanto umap.emplace(11, d)não o fez.

Epílogo:

I. Observe que uma sobrecarga de insert()é realmente equivalente a emplace() . Conforme descrito nesta página do cppreference.com , a sobrecarga template<class P> std::pair<iterator, bool> insert(P&& value)(que é a sobrecarga (2) insert()nesta página do cppreference.com) é equivalente a emplace(std::forward<P>(value)).

II Para onde ir daqui?

uma. Brinque com o código fonte acima e estude a documentação para insert()(por exemplo, aqui ) e emplace()(por exemplo, aqui ) encontrada on-line. Se você estiver usando um IDE como o eclipse ou o NetBeans, poderá facilmente fazer com que o IDE informe qual sobrecarga insert()ou emplace()está sendo chamada (no eclipse, mantenha o cursor do mouse estável durante a chamada de função por um segundo). Aqui está mais um código para experimentar:

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's only difference from 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's instructive to find out why. The two calls
// 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}}));

Você verá em breve que a sobrecarga do std::pairconstrutor (consulte a referência ) acaba sendo usada unordered_mappode ter um efeito importante em quantos objetos são copiados, movidos, criados e / ou destruídos, bem como quando tudo isso ocorre.

b. Veja o que acontece quando você usa alguma outra classe de contêiner (por exemplo, std::setou std::unordered_multiset) em vez de std::unordered_map.

c. Agora use um Gooobjeto (apenas uma cópia renomeada de Foo) em vez de um intcomo o tipo de intervalo em um unordered_map(ou seja, use em unordered_map<Foo, Goo>vez de unordered_map<Foo, int>) e veja quantos e quais Gooconstrutores são chamados. (Spoiler: há um efeito, mas não é muito dramático.)

Matthew K.
fonte