O comitê de padrões C ++ pretende que em C ++ 11 unordered_map destrua o que insere?

114

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:

  1. 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.

  2. Este problema é amplamente conhecido, porque com certeza atualizar os compiladores fará com que o código que costumava funcionar pare de funcionar repentinamente?

  3. O comitê de padrões C ++ pretendia este comportamento?

  4. 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.

Niall Douglas
fonte
10
Só porque ele usa um ref universal não significa que o valor inserido é sempre movido - ele só deve fazer isso para rvalues, o que anão é o caso. Deve fazer uma cópia. Além disso, esse comportamento depende totalmente do stdlib, não do compilador.
Xeo
10
Isso parece um bug na implementação da biblioteca
David Rodríguez - dribeas
4
"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." Desculpe, mas você está errado. De que parte do C ++ Standard você tirou essa ideia? BTW cplusplus.com não é oficial.
Ben Voigt
1
Não consigo reproduzir isso no meu sistema e estou usando o gcc 4.8.2 e 4.9.0 20131223 (experimental)respectivamente. A saída é a.second is now 0x2074088 (ou semelhante) para mim.
47
Este foi o bug 57619 do GCC , uma regressão na série 4.8 que foi corrigida para 4.8.2 em 2013-06.
Casey

Respostas:

83

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

  template<typename _Pair, typename = typename
           std::enable_if<std::is_constructible<value_type,
                                                _Pair&&>::value>::type>
    std::pair<iterator, bool>
    insert(_Pair&& __x)
    { return _M_t._M_insert_unique(std::forward<_Pair>(__x)); }

bits/unordered_map.h, linhas 365-370

  template<typename _Pair, typename = typename
           std::enable_if<std::is_constructible<value_type,
                                                _Pair&&>::value>::type>
    std::pair<iterator, bool>
    insert(_Pair&& __x)
    { return _M_h.insert(std::move(__x)); }

O último está usando incorretamente std::moveonde deveria estar std::forward.

Brian
fonte
11
Clang usa libstdc ++ por padrão, stdlib do GCC.
Xeo
Estou usando o gcc 4.9 e estou procurando 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.
Sim, acho que eles consertaram o bug.
Brian
@Brian Não, acabei de verificar os cabeçalhos do meu sistema. Mesma coisa. 4.8.2 btw.
O meu é 4.8.1, então acho que foi corrigido entre os dois.
Brian
20
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.

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.

Então, por que nosso código acima escolheu esta sobrecarga, e não a sobrecarga unordered_map :: value_type como provavelmente muitos esperariam?

Porque você, como muitas outras pessoas antes, interpretou mal o value_typeno contêiner. O value_typede *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:

iterator       insert(const_iterator hint, const value_type& obj);
David Rodríguez - dribeas
fonte
Ainda não entendi por que existe esse novo lema, "referência universal", não significa nada específico, nem carrega um bom nome para algo que não é totalmente "universal" na prática. É muito melhor lembrar algumas regras de recolhimento antigas que fazem parte do comportamento da meta-linguagem C ++ como eram desde que os modelos foram introduzidos na linguagem, além de uma nova assinatura do padrão C ++ 11. Qual é a vantagem de falar sobre essas referências universais, afinal? Já existem nomes e coisas que são estranhas o suficiente, como std::movese não movessem nada.
user2485710
2
@ user2485710 Às vezes, a ignorância é uma bênção, e ter o termo genérico "referência universal" em todo o colapso de referência e ajuste de dedução de tipo de modelo que é IMHO muito pouco intuitivo, além do requisito de usar std::forwardpara 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).
Mark Garcia
1
Em meu pensamento, "referência universal" é um padrão para declarar parâmetros de modelo de função que podem ser vinculados a lvalues ​​e rvalues. "Colapso de referência" é o que acontece ao substituir parâmetros de modelo (deduzidos ou especificados) em definições, em situações de "referência universal" e outros contextos.
aschepler
2
@aschepler: referência universal é apenas um nome sofisticado para um subconjunto de colapsos de referência. Concordo que a ignorância é uma benção , e também o fato de ter um nome chique torna a conversa mais fácil e moderna e que pode ajudar a propagar o comportamento. Dito isto, não sou um grande fã do nome, pois empurra as regras reais para um canto onde não precisam ser conhecidas ... isto é, até que você encontre um caso diferente do que Scott Meyers descreveu.
David Rodríguez - dribeas
Semântica inútil agora, mas a distinção que eu estava tentando sugerir: a referência universal acontece quando estou projetando e declarando um parâmetro de função como parâmetro de modelo dedutível &&; 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.
aschepler