È inteso dal comitato per gli standard C ++ che in C ++ 11 unordered_map distrugge ciò che inserisce?


114

Ho appena perso tre giorni della mia vita rintracciando uno strano bug in cui unordered_map :: insert () distrugge la variabile che inserisci. Questo comportamento altamente non ovvio si verifica solo in compilatori molto recenti: ho scoperto che clang 3.2-3.4 e GCC 4.8 sono gli unici compilatori a dimostrare questa "caratteristica".

Ecco un po 'di codice ridotto dalla mia base di codice principale che dimostra il 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;
}

Io, come probabilmente la maggior parte dei programmatori C ++, mi aspetto che l'output assomigli a questo:

a.second is 0x8c14048
a.second is now 0x8c14048

Ma con clang 3.2-3.4 e GCC 4.8 ottengo invece questo:

a.second is 0xe03088
a.second is now 0

Il che potrebbe non avere senso, fino a quando non esaminerai attentamente i documenti per unordered_map :: insert () su http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/ dove l'overload n. 2 è:

template <class P> pair<iterator,bool> insert ( P&& val );

Che è un avido sovraccarico di movimento di riferimento universale, che consuma tutto ciò che non corrisponde a nessuno degli altri sovraccarichi e si sposta costruendolo in un valore_tipo. Allora perché il nostro codice sopra ha scelto questo sovraccarico e non il sovraccarico unordered_map :: value_type come probabilmente la maggior parte si aspetterebbe?

La risposta ti guarda in faccia: unordered_map :: value_type è una coppia < const int, std :: shared_ptr> e il compilatore penserebbe correttamente che una coppia < int , std :: shared_ptr> non è convertibile. Pertanto il compilatore sceglie il sovraccarico del riferimento universale di spostamento, e questo distrugge l'originale, nonostante il programmatore non utilizzi std :: move () che è la tipica convenzione per indicare che sei a posto con una variabile che viene distrutta. Pertanto, il comportamento di distruzione dell'inserimento è di fatto corretto secondo lo standard C ++ 11 ei compilatori più vecchi non erano corretti .

Probabilmente ora puoi vedere perché ho impiegato tre giorni per diagnosticare questo bug. Non era affatto ovvio in una grande base di codice in cui il tipo da inserire in unordered_map era un typedef definito lontano in termini di codice sorgente, e non è mai venuto in mente a nessuno di controllare se il typedef fosse identico a value_type.

Quindi le mie domande a Stack Overflow:

  1. Perché i compilatori più vecchi non distruggono le variabili inserite come i compilatori più recenti? Voglio dire, anche GCC 4.7 non lo fa ed è piuttosto conforme agli standard.

  2. Questo problema è ampiamente noto, perché sicuramente l'aggiornamento dei compilatori farà smettere improvvisamente di funzionare il codice che prima funzionava?

  3. Il comitato per gli standard C ++ intendeva questo comportamento?

  4. Come suggeriresti di modificare unordered_map :: insert () per dare un comportamento migliore? Lo chiedo perché se qui c'è supporto, intendo inviare questo comportamento come una nota N al WG21 e chiedere loro di implementare un comportamento migliore.


10
Solo perché usa un riferimento universale non significa che il valore inserito sia sempre spostato - dovrebbe farlo solo per i valori, che anon lo è. Dovrebbe fare una copia. Inoltre, questo comportamento dipende totalmente dallo stdlib, non dal compilatore.
Xeo

10
Sembra un bug nell'implementazione della libreria
David Rodríguez - dribeas

4
"Pertanto, il comportamento di distruzione dell'inserimento è di fatto corretto secondo lo standard C ++ 11 e i vecchi compilatori non erano corretti." Scusa, ma ti sbagli. Da quale parte dello standard C ++ ti è venuta l'idea? BTW cplusplus.com non è ufficiale.
Ben Voigt

1
Non riesco a riprodurlo sul mio sistema e sto usando 4.9.0 20131223 (experimental)rispettivamente gcc 4.8.2 e . L'output è a.second is now 0x2074088 (o simile) per me.

47
Si trattava del bug GCC 57619 , una regressione nella serie 4.8 che è stata corretta per 4.8.2 nel 2013-06.
Casey

Risposte:


83

Come altri hanno sottolineato nei commenti, il costruttore "universale" non dovrebbe, infatti, spostarsi sempre dal suo argomento. Dovrebbe spostarsi se l'argomento è davvero un valore e copiare se è un valore.

Il comportamento, osservi, che si muove sempre, è un bug in libstdc ++, che ora viene corretto in base a un commento alla domanda. Per i curiosi, ho dato un'occhiata alle intestazioni g ++ - 4.8.

bits/stl_map.h, linee 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, linee 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)); }

Quest'ultimo sta usando in modo errato std::movedove dovrebbe essere usato std::forward.


11
Clang usa libstdc ++ per impostazione predefinita, stdlib di GCC.
Xeo

Sto usando gcc 4.9 e sto cercando in libstdc++-v3/include/bits/. Non vedo la stessa cosa. Capisco { return _M_h.insert(std::forward<_Pair>(__x)); }. Potrebbe essere diverso per 4.8, ma non ho ancora controllato.

Sì, quindi immagino che abbiano risolto il bug.
Brian

@ Brian No, ho appena controllato le intestazioni di sistema. Stessa cosa. 4.8.2 btw.

Il mio è 4.8.1, quindi immagino che sia stato risolto tra i due.
Brian

20
template <class P> pair<iterator,bool> insert ( P&& val );

Che è un avido sovraccarico di movimento di riferimento universale, che consuma tutto ciò che non corrisponde a nessuno degli altri sovraccarichi e si sposta costruendolo in un valore_tipo.

Questo è ciò che alcune persone chiamano riferimento universale , ma in realtà il riferimento sta collassando . Nel tuo caso, dove l'argomento è un lvalue di tipo pair<int,shared_ptr<int>>, non risulterà nell'argomento un riferimento rvalue e non dovrebbe spostarsi da esso.

Allora perché il nostro codice sopra ha scelto questo sovraccarico e non il sovraccarico unordered_map :: value_type come probabilmente la maggior parte si aspetterebbe?

Perché tu, come molte altre persone prima, hai interpretato male value_typeil contenuto del contenitore. Il value_typedi *map(sia ordinato che non ordinato) è pair<const K, T>, che nel tuo caso è pair<const int, shared_ptr<int>>. Il tipo che non corrisponde elimina il sovraccarico che potresti aspettarti:

iterator       insert(const_iterator hint, const value_type& obj);

Ancora non capisco perché esista questo nuovo lemma, "riferimento universale", non significhi realmente nulla di specifico, né abbia un buon nome per una cosa che non è totalmente "universale" nella pratica. È molto meglio ricordare alcune vecchie regole di compressione che fanno parte del comportamento del meta-linguaggio C ++ com'era da quando i modelli sono stati introdotti nel linguaggio, oltre a una nuova firma dallo standard C ++ 11. Qual è comunque il vantaggio di parlare di questi riferimenti universali? Ci sono già nomi e cose che sono abbastanza strane, come std::movese non muovesse nulla.
user2485710

2
@ user2485710 A volte, l'ignoranza è una gioia, e avere il termine generico "riferimento universale" su tutti i crolli di riferimento e il tweak di deduzione del tipo di modello che IMHO è piuttosto poco intuitivo, oltre al requisito da utilizzare std::forwardper fare in modo che quel tweak faccia il vero lavoro ... Scott Meyers ha svolto un buon lavoro stabilendo regole piuttosto semplici per l'inoltro (l'uso di riferimenti universali).
Mark Garcia

1
Secondo me, "riferimento universale" è un modello per dichiarare i parametri del modello di funzione che possono legarsi sia a lvalues ​​che a rvalues. Il "collasso dei riferimenti" è ciò che accade quando si sostituiscono i parametri del modello (dedotti o specificati) nelle definizioni, nelle situazioni di "riferimento universale" e in altri contesti.
aschepler

2
@aschepler: il riferimento universale è solo un nome di fantasia per un sottoinsieme di riferimenti che collassano. Sono d'accordo con l' ignoranza è una gioia , e anche il fatto che avere un nome di fantasia rende più facile e alla moda parlarne e questo potrebbe aiutare a propagare il comportamento. Detto questo, non sono un grande fan del nome, poiché spinge le regole effettive in un angolo in cui non hanno bisogno di essere conosciute ... cioè, fino a quando non si colpisce un caso al di fuori di ciò che ha descritto Scott Meyers.
David Rodríguez - dribeas

Semantica inutile ora, ma la distinzione che stavo cercando di suggerire: il riferimento universale si verifica quando progetto e dichiaro un parametro di funzione come parametro-modello-deducibile &&; la compressione dei riferimenti si verifica quando un compilatore crea un'istanza di un modello. Il collasso dei riferimenti è la ragione per cui i riferimenti universali funzionano, ma al mio cervello non piace mettere i due termini nello stesso dominio.
aschepler
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.