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


114

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

この問題を示す、私のメインコードベースからの削減されたコードを次に示します。

#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/にあるunordered_map :: insert()のドキュメントを詳しく調べるまで意味がありません。

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

これは、他のオーバーロードのいずれかに一致しないものを消費し、貪欲普遍参照ムーブオーバーロードされ、そして構築移動 VALUE_TYPEにそれを。では、なぜ上記のコードがこのオーバーロードを選択したのでしょうか?

答えはあなたを正面から見ています:unordered_map :: value_typeはペア< const int、std :: shared_ptr>であり、コンパイラーはペア< int、std :: shared_ptr>は変換可能ではないと正しく認識します。したがって、プログラマーがstd :: move()を使用していないにもかかわらず、コンパイラーはユニバーサルムーブリファレンスのオーバーロードを選択し、元のオブジェクトを破棄します。そのための行動を破壊し、インサートは、実際には正しい C ++ 11標準あたりとして、そして古いコンパイラであった正しくありません

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

スタックオーバーフローへの私の質問:

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

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

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

  4. unordered_map :: insert()を変更して動作を改善することをどのように提案しますか?ここでサポートがある場合は、この動作をWG21へのNノートとして送信し、より良い動作を実装するように依頼するつもりなので、これを質問します。


10
それが普遍的な参照を使用するからといって、挿入された値が常に移動されることを意味するわけではありません-それ、rvaluesに対してのみそうするべきaです。コピーを作成する必要があります。また、この動作は、コンパイラではなくstdlibに完全に依存しています。
Xeo

10
これは、ライブラリの実装のバグのように思える
デビッド・ロドリゲス- dribeas

4
「したがって、挿入破壊動作は実際にはC ++ 11標準に従って正しく、古いコンパイラは正しくありませんでした。」申し訳ありませんが、あなたは間違っています。C ++標準のどの部分からそのアイデアを得ましたか?ところで、cplusplus.comは公式ではありません。
Ben Voigt 2014

1
私のシステムではこれを再現できず、4.9.0 20131223 (experimental)それぞれgcc 4.8.2を使用しています。出力はa.second is now 0x2074088 (または同様の)です。

47
これはGCCバグ57619であり、4.8シリーズのリグレッションであり、2013年6月に4.8.2で修正されました。
ケーシー

回答:


83

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

あなたが観察する行動は、常に動きますが、libstdc ++のバグであり、質問へのコメントに従って修正されました。それらの好奇心のために、私は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


11
Clangはデフォルトでlibstdc ++を使用します。GCCのstdlibです。
Xeo

私はgcc 4.9を使用していlibstdc++-v3/include/bits/ます。私は同じことを見ていません。なるほど{ return _M_h.insert(std::forward<_Pair>(__x)); }。4.8では異なる可能性がありますが、まだ確認していません。

ええ、彼らはバグを修正したと思います。
ブライアン

@Brian Nope、システムヘッダーを確認しました。同じこと。4.8.2 btw。

私のは4.8.1なので、2つの間で修正されたと思います。
ブライアン

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

これは、貪欲なユニバーサル参照移動オーバーロードであり、他のオーバーロードと一致しないものを消費し、それをvalue_typeに移動します。

これは一部の人々が普遍的な参照と呼んでいるものですが、実際には参照が崩壊しています。あなたのケースでは、引数がどこにある左辺値の型のpair<int,shared_ptr<int>>それはなりません引数は右辺値参照であることになり、それはいけません、そこから移動します。

では、なぜ上記のコードがこのオーバーロードを選択したのでしょうか?

あなたは、他の多くの人々と同様value_typeに、コンテナ内のを誤って解釈したためです。value_typeのは*map(注文または順不同かどうか)であるpair<const K, T>あなたのケースです、pair<const int, shared_ptr<int>>。タイプが一致しないと、予想されるオーバーロードが解消されます。

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

この新しい見出しが存在する理由はまだわかりません。「普遍的な参照」は、具体的に何も意味しておらず、実際には完全に「普遍的」ではないものに良い名前を付けていません。テンプレートが言語で導入されて以来のC ++メタ言語の動作の一部であるいくつかの古い折りたたみルールと、C ++ 11標準からの新しいシグネチャを覚えておく方がはるかに良いです。とにかくこの普遍的な参照について話すことの利点は何ですか?名前や変なものはすでにありstd::moveます。それは何も動かしません。
user2485710 2014

2
@ user2485710時には、無知は至福であり、すべての参照の折りたたみとテンプレートタイプの演繹に関する包括的用語「普遍的な参照」は、私見ではかなり直感的でstd::forwardはありません。転送(かなりの単純なルール)の設定​​(汎用参照の使用)は適切に行われています。
Mark Garcia、

1
私の考えでは、「ユニバーサルリファレンス」は、左辺値と右辺値の両方にバインドできる関数テンプレートパラメータを宣言するためのパターンです。「参照の折りたたみ」とは、「汎用参照」の状況やその他のコンテキストで、テンプレートのパラメーターを定義に代入(推定または指定)したときに起こることです。
aschepler

2
@aschepler:ユニバーサル参照は、参照の折りたたみのサブセットの単なる仮名です。私は無知が至福であることに同意します。また、派手な名前を付けることでそれについて話しやすくなり、流行が広がり、行動を広めるのに役立つ可能性があるという事実にも同意します。そうは言っても、実際のルールが知らされる必要がないコーナーに押しやられるので、私はその名前の大ファンではありません。つまり、スコットマイヤーズが説明した以外のケースにぶつかるまでは。
デビッドロドリゲス-14

今は無意味なセマンティクスですが、私が提案しようとした違いは次のとおりです。汎用参照は、関数パラメーターをdeducible-template-parameterとして設計および宣言するときに発生します&&。参照の折りたたみは、コンパイラがテンプレートをインスタンス化するときに発生します。参照の折りたたみがユニバーサル参照が機能する理由ですが、私の脳は2つの用語を同じドメインに置くことを好みません。
aschepler
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.