web-dev-qa-db-ja.com

C ++ 11でunordered_mapが挿入したものを破壊するのはC ++標準委員会の意図ですか?

Unordered_map :: insert()が挿入した変数を破壊するという非常に奇妙なバグを追跡して、私の人生の3日間を失いました。この非常に非自明な動作はごく最近のコンパイラでのみ発生します。clang3.2-3.4およびGCC 4.8は、この「機能」を実証するonlyコンパイラであることがわかりました。 「。

以下に、この問題を実証するメインコードベースの一部のコードを示します。

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

私は、おそらくほとんどのC++プログラマと同様に、出力が次のようになることを期待します。

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

しかし、clang 3.2-3.4とGCC 4.8では、代わりに次のようになります:

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

http://www.cplusplus.com/reference/unordered_map/unordered_map/insert/ のオーバーロードなし2でunordered_map :: insert()のドキュメントを詳しく調べるまで、これは意味がないかもしれません:

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

これは貪欲な普遍的な参照移動オーバーロードであり、他のオーバーロードのいずれにも一致しないものを消費し、それをvalue_typeに構築する移動します。では、上のコードでは、おそらくほとんどの人が期待するunordered_map :: value_typeオーバーロードではなく、このオーバーロードを選択したのはなぜですか?

答えは顔を凝視します:unordered_map :: value_typeはpair <constint、std :: shared_ptr>であり、コンパイラーはpair <int、std :: shared_ptr>は変換できません。したがって、コンパイラはmove Universal Reference Overloadを選択し、元のを破壊しますが、プログラマは標準的な規則であるstd :: move()を使用していません変数が破壊されても問題ないことを示します。したがって、挿入の破壊動作は、実際にはC++ 11標準に従ってcorrectであり、古いコンパイラはincorrectでした。

おそらく、このバグを診断するのに3日かかった理由がおわかりでしょう。 unordered_mapに挿入される型がソースコードの用語で遠く離れて定義されたtypedefであり、typedefがvalue_typeと同一であるかどうかを確認することは誰にも起こりませんでした。

スタックオーバーフローに対する私の質問:

  1. 古いコンパイラが新しいコンパイラのように挿入された変数を破壊しないのはなぜですか?つまり、GCC 4.7でもこれは行われず、かなり標準に準拠しています。

  2. 確かにコンパイラをアップグレードすると、以前は動作していたコードが突然動作しなくなるため、この問題は広く知られていますか?

  3. C++標準委員会はこの動作を意図していましたか?

  4. Unordered_map :: insert()を変更してより良い動作を与えるようにするにはどうすればよいですか?ここでサポートがある場合は、この動作をWG21にNメモとして送信し、より良い動作を実装するように依頼するため、これをお願いします。

113
Niall Douglas

他の人がコメントで指摘しているように、「ユニバーサル」コンストラクターは、実際には、常にその引数から移動するように想定されていません。引数が実際に右辺値の場合は移動し、左辺値の場合はコピーすることになっています。

常に移動する動作は、libstdc ++のバグであり、質問に対するコメントに従って修正されています。好奇心those盛な人のために、g ++-4.8ヘッダーを調べました。

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

後者はstd::moveを使用するはずの場所でstd::forwardを誤って使用しています。

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

これは貪欲な普遍的な参照の移動オーバーロードであり、他のオーバーロードのいずれとも一致しないものを消費し、それをvalue_typeに構築します。

それは一部の人々がユニバーサルリファレンスと呼んでいるものですが、実際はリファレンス折りたたみ 。あなたの場合、引数がタイプpair<int,shared_ptr<int>>lvalueである場合、引数はnotになります右辺値参照とそれは、そこから移動すべきではありません

では、上のコードでは、おそらくほとんどの人が期待するunordered_map :: value_typeオーバーロードではなく、このオーバーロードを選択したのはなぜですか?

あなたは、他の多くの人々と同じように、コンテナのvalue_typeを誤って解釈したためです。 value_type*map(順序付きまたは順序なし)はpair<const K, T>です。これは、あなたの場合はpair<const int, shared_ptr<int>>です。一致しない型により、予想される過負荷が解消されます。

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