Acabei de perder três dias da minha vida rastreando um bug muito estranho onde unordered_map :: insert () destrói a variável que você inseriu. Esse comportamento altamente não óbvio ocorre apenas em compiladores muito recentes: descobri que o clang 3.2-3.4 e o GCC 4.8 são os únicos compiladores a demonstrar esse "recurso".
Aqui está um código reduzido da minha base de código principal que demonstra o problema:
#include <memory>
#include <unordered_map>
#include <iostream>
int main(void)
{
std::unordered_map<int, std::shared_ptr<int>> map;
auto a(std::make_pair(5, std::make_shared<int>(5)));
std::cout << "a.second is " << a.second.get() << std::endl;
map.insert(a); // Note we are NOT doing insert(std::move(a))
std::cout << "a.second is now " << a.second.get() << std::endl;
return 0;
}
Eu, como provavelmente a maioria dos programadores C ++, esperaria que a saída se parecesse com isto:
a.second is 0x8c14048
a.second is now 0x8c14048
Mas com o clang 3.2-3.4 e GCC 4.8, eu recebo isto em vez disso:
a.second is 0xe03088
a.second is now 0
O que pode não fazer sentido, até que você examine de perto os documentos de unordered_map :: insert () em http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/, onde a sobrecarga no 2 é:
template <class P> pair<iterator,bool> insert ( P&& val );
Que é uma sobrecarga de movimento de referência universal gananciosa, consumindo qualquer coisa que não corresponda a nenhuma das outras sobrecargas e mova construindo -o em um value_type. Então, por que nosso código acima escolheu essa sobrecarga, e não a sobrecarga unordered_map :: value_type como provavelmente a maioria esperaria?
A resposta o encara: unordered_map :: value_type é um par < const int, std :: shared_ptr> e o compilador pensaria corretamente que um par < int , std :: shared_ptr> não é conversível. Portanto, o compilador escolhe a sobrecarga de referência universal move, e isso destrói o original, apesar do programador não usar std :: move () que é a convenção típica para indicar que você está bem com uma variável sendo destruída. Portanto, o comportamento de destruição de inserções está de fato correto de acordo com o padrão C ++ 11, e os compiladores mais antigos estavam incorretos .
Você provavelmente pode ver agora por que levei três dias para diagnosticar esse bug. Não era nada óbvio em uma grande base de código onde o tipo sendo inserido em unordered_map era um typedef definido muito longe em termos de código-fonte, e nunca ocorreu a ninguém verificar se o typedef era idêntico a value_type.
Então, minhas perguntas para Stack Overflow:
Por que os compiladores mais antigos não destroem as variáveis inseridas como os compiladores mais novos? Quer dizer, mesmo o GCC 4.7 não faz isso, e está em conformidade com os padrões.
Este problema é amplamente conhecido, porque com certeza atualizar os compiladores fará com que o código que costumava funcionar pare de funcionar repentinamente?
O comitê de padrões C ++ pretendia este comportamento?
Como você sugere que unordered_map :: insert () seja modificado para fornecer um melhor comportamento? Eu pergunto isso porque, se houver suporte aqui, pretendo enviar esse comportamento como uma nota N ao WG21 e pedir que implementem um comportamento melhor.
a
não é o caso. Deve fazer uma cópia. Além disso, esse comportamento depende totalmente do stdlib, não do compilador.4.9.0 20131223 (experimental)
respectivamente. A saída éa.second is now 0x2074088
(ou semelhante) para mim.Respostas:
Como outros apontaram nos comentários, o construtor "universal" não deve, de fato, se mover sempre de seu argumento. Ele deve mover se o argumento for realmente um rvalue e copiar se for um lvalue.
O comportamento, você observa, que sempre se move, é um bug no libstdc ++, que agora é corrigido de acordo com um comentário sobre a questão. Para os curiosos, dei uma olhada nos cabeçalhos g ++ - 4.8.
bits/stl_map.h
, linhas 598-603bits/unordered_map.h
, linhas 365-370O último está usando incorretamente
std::move
onde deveria estarstd::forward
.fonte
libstdc++-v3/include/bits/
. Eu não vejo a mesma coisa. Eu vejo{ return _M_h.insert(std::forward<_Pair>(__x)); }
. Pode ser diferente no 4.8, mas ainda não verifiquei.Isso é o que algumas pessoas chamam de referência universal , mas na verdade é o colapso da referência . No seu caso, onde o argumento é um lvalue do tipo
pair<int,shared_ptr<int>>
, não resultará no argumento sendo uma referência rvalue e não deve se mover a partir dele.Porque você, como muitas outras pessoas antes, interpretou mal o
value_type
no contêiner. Ovalue_type
de*map
(ordenado ou não) épair<const K, T>
, que no seu caso épair<const int, shared_ptr<int>>
. O tipo não correspondente elimina a sobrecarga que você pode estar esperando:fonte
std::move
se não movessem nada.std::forward
para fazer esse ajuste fazer o trabalho real ... Scott Meyers fez um bom trabalho definindo regras bastante diretas para encaminhamento (o uso de referências universais).&&
; o colapso de referência acontece quando um compilador instancia um modelo. O colapso de referências é o motivo pelo qual as referências universais funcionam, mas meu cérebro não gosta de colocar os dois termos no mesmo domínio.