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_type
ou 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:
Qual é a vantagem de cada um deles em relação aos outros?
Havia necessidade de adicionar lugar ao padrão? Existe algo que antes não era possível sem ele?
operator[]
é baseadotry_emplace
. Também vale a pena mencionarinsert_or_assign
.Respostas:
No caso particular de um mapa, as opções antigas eram apenas duas:
operator[]
einsert
(sabores diferentes deinsert
). 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
insert
função (no tipo de elemento único) pega umvalue_type
(std::pair<const Key,Value>
), usa a chave (first
membro) e tenta inseri-la. Comostd::map
nã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. Ainsert
função não modifica o estado do mapa, mas retorna um iterador para o elemento (e umfalse
indicando que não foi inserido).No caso do
insert
argumento, é um objeto devalue_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 elevalue_type
possa ser construído, e é aí questd::make_pair
entra em cena, pois permite a criação simples destd::pair
objetos, embora provavelmente não seja o que você deseja ...O efeito líquido das seguintes chamadas é semelhante :
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 ainsert
função. Ainsert
função criará o nó apropriado na árvore de pesquisa binária e copiará avalue_type
parte do argumento para o nó. A vantagem de usarvalue_type
é que, bem,value_type
sempre correspondevalue_type
, você não pode digitar errado o tipo dosstd::pair
argumentos!A diferença está em [3]. A função
std::make_pair
é uma função de modelo que criará umstd::pair
. A assinatura é: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 sejaT==K,U==V
, portanto a chamada parastd::make_pair
retornará astd::pair<K,V>
(observe a faltaconst
). A assinatura requervalue_type
que seja próximo, mas não o mesmo que o valor retornado da chamada parastd::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:
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
insert
que exigem a criaçãovalue_type
externa e a cópia desse objeto no contêiner. Como alternativa, você pode usaroperator[]
se o tipo é padrão construtível e atribuível (focalizando intencionalmente apenas emm[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
emplace
funçõ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.Em [5], o
std::pair<const K, V>
não é criado e passado paraemplace
, mas as referências ao objetot
eu
são passadas paraemplace
que os encaminha ao construtor dovalue_type
subobjeto dentro da estrutura de dados. Nesse caso, nenhuma cópiastd::pair<const K,V>
é feita, o que é a vantagem dasemplace
alternativas C ++ 03. Como no casoinsert
, não substituirá o valor no mapa.Uma pergunta interessante sobre a qual eu não havia pensado é como
emplace
realmente pode ser implementado para um mapa, e esse não é um problema simples no caso geral.fonte
mapped_type
cópia de segurança. O que queremos é substituir a construção domapped_type
no par e substituir a construção do par no mapa. Portanto, astd::pair::emplace
função e seu suporte de encaminhamentomap::emplace
estã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.insert_or_assign
etry_emplace
(ambas do C ++ 17), que ajudam a preencher algumas lacunas na funcionalidade dos métodos existentes.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.
fonte
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 osmap
argumentos que necessita para criá-lo dentro de si. Você não faz o objeto.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:
É um detalhe muito específico, mas quando você lida com cadeias de conversões definidas pelo usuário, vale a pena lembrar disso.
fonte
v.emplace(v.end(), 10, 10);
... ou agora você precisaria usarv.emplace(v.end(), foo(10, 10) );
:?emplace
usam 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.O código a seguir pode ajudar você a entender a "ideia geral" de como
insert()
difereemplace()
:A saída que obtive foi:
Notar que:
Um
unordered_map
sempre armazena internamenteFoo
objetos (e não, digamos,Foo *
s) como chaves, que são todos destruídos quando o objetounordered_map
é destruído. Aqui, asunordered_map
chaves internas da empresa eram foos 13, 11, 5, 10, 7 e 9.unordered_map
realmente armazenastd::pair<const Foo, int>
objetos, que por sua vez, armazenam osFoo
objetos. Mas, para entender a "ideia geral" de comoemplace()
difereinsert()
(veja a caixa destacada abaixo), não há problema em imaginar temporariamente essestd::pair
objeto como sendo inteiramente passivo. Depois de entender essa "ideia geral", é importante fazer backup e entender como o uso dessestd::pair
objeto intermediáriounordered_map
introduz sutis, mas importantes, aspectos técnicos.Inserindo cada uma
foo0
,foo1
efoo2
necessário 2 chamadas para um dosFoo
construtores de copiar / mover s e 2 chamadas paraFoo
's destructor (como eu agora descrever):uma. A inserção de cada um
foo0
efoo1
criou um objeto temporário (foo4
efoo6
, respectivamente) cujo destruidor foi chamado imediatamente após a conclusão da inserção. Além disso, osFoo
s internos do unordered_map (que sãoFoo
s 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 (chamadopair
), que chamouFoo
o construtor de cópias onfoo2
(criandofoo8
como um membro interno depair
). Em seguida,insert()
editamos esse par, o que resultou emunordered_map
chamar o construtor de cópia novamente (onfoo8
) para criar sua própria cópia interna (foo9
). Assim comofoo
s 0 e 1, o resultado final foram duas chamadas de destruidor para essa inserção, com a única diferençafoo8
: o destruidor foi chamado apenas quando chegamos ao final domain()
que em ser chamado imediatamente após oinsert()
término.A colocação
foo3
resultou em apenas 1 chamada de copiar / mover do construtor (criandofoo10
internamente nounordered_map
) e apenas 1 chamada noFoo
destruidor. (Voltarei a isso mais tarde).Pois
foo11
, passamos diretamente o número inteiro 11 paraemplace(11, d)
queunordered_map
chamaria oFoo(int)
construtor enquanto a execução estivesse dentro de seuemplace()
método. Ao contrário de (2) e (3), nem precisávamos de umfoo
objeto pré-existente para fazer isso. Importante, observe queFoo
ocorreu apenas 1 chamada para um construtor (que foi criadofoo11
).Passamos diretamente o número inteiro 12 para
insert({12, d})
. Diferentemente deemplace(11, d)
(que a retirada resultou em apenas 1 chamada para umFoo
construtor), essa chamadainsert({12, d})
resultou em duas chamadas paraFoo
o construtor (criandofoo12
efoo13
).Isso mostra qual é a principal diferença "grande" entre
insert()
eemplace()
é:Nota: O motivo do " quase " em " quase sempre " acima é explicado em I) abaixo.
umap.emplace(foo3, d)
chamadoFoo
é o seguinte: Como estamos usandoemplace()
, o compilador sabe quefoo3
(umFoo
objeto não const ) deve ser um argumento para algumFoo
construtor. Nesse caso, oFoo
construtor mais adequado é o construtor de cópia não constFoo(Foo& f2)
. É por isso queumap.emplace(foo3, d)
chamou um construtor de cópias enquantoumap.emplace(11, d)
não o fez.Epílogo:
I. Observe que uma sobrecarga de
insert()
é realmente equivalente aemplace()
. Conforme descrito nesta página do cppreference.com , a sobrecargatemplate<class P> std::pair<iterator, bool> insert(P&& value)
(que é a sobrecarga (2)insert()
nesta página do cppreference.com) é equivalente aemplace(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 ) eemplace()
(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 sobrecargainsert()
ouemplace()
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:Você verá em breve que a sobrecarga do
std::pair
construtor (consulte a referência ) acaba sendo usadaunordered_map
pode 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::set
oustd::unordered_multiset
) em vez destd::unordered_map
.c. Agora use um
Goo
objeto (apenas uma cópia renomeada deFoo
) em vez de umint
como o tipo de intervalo em umunordered_map
(ou seja, use emunordered_map<Foo, Goo>
vez deunordered_map<Foo, int>
) e veja quantos e quaisGoo
construtores são chamados. (Spoiler: há um efeito, mas não é muito dramático.)fonte
Em termos de funcionalidade ou saída, ambos são iguais.
Para memória grande, o objeto emplace é otimizado para memória, que não usa construtores de cópia
Para uma explicação detalhada e simples, https://medium.com/@sandywits/all-about-emplace-in-c-71fd15e06e44
fonte