21

このコードを MS Visual C++ 2010 に入れ、コンパイル (デバッグまたはリリース) すると、insert() ループでクラッシュしますが、push_back ループではクラッシュしません。

#include <vector>
#include <string>

using std::vector;
using std::string;

int main()
{
   vector<string> vec1;
   vec1.push_back("hello");

   for (int i = 0; i != 10; ++i)
      vec1.push_back( vec1[0] );

   vector<string> vec2;
   vec2.push_back("hello");

   for (int i = 0; i != 10; ++i)
      vec2.insert( vec2.end(), vec2[0] );

   return 0;
}

問題は、push_back() と insert() の両方が参照によって新しい項目を取得し、ベクトルがより多くのスペースのために再割り当てされると、新しい項目が挿入される前に無効になることです。

GCC にもこの問題があるはずです。Clang はチェックしていませんが、使用している STD ライブラリによって異なります。

MSVC2010 の push_back() には、新しい項目が実際にベクター内の項目であるかどうかを検出する追加のコードがあります。その場合、アイテムのインデックスを記録し、それを使用して、メモリが割り当てられた後にアイテムを挿入します (無効になった参照を使用する代わりに) -- _Inside(_STD addressof(_Val)) を使用します。

MSVC の追加コードは非標準ですか?

私の懸念は、どのコードで vec.push_back(vec[1]); のようなことを行ったのかわからないことです。または vec.insert(それ、vec[2]); push_back と insert を使用する数千行ではないにしても、数百行のコードを調べる必要があります。これは単なる私自身のコードです... サードパーティのライブラリも影響を受ける可能性があります。

この手法を使用すると、GCC が恐ろしい方法で停止する可能性があると思います (このケースを処理するための追加のコードはありませんが、valgrind は私の単純な例ではそれを検出しなかったため、テストが難しくなります)、

この間違いを検出して回避するにはどうすればよいでしょうか?

MSVC2010 の余分な push_back() コードは非標準ですか? MSVC は代わりに、この方法で使用されているベクトルを検出してアサートする必要がありますか? (つまり、セキュア コンピューティング イニシアチブ)

これらのケースを検出するために、MSVC2010 と GCC のヘッダーをハッキングすることを考えています。

他のアイデアはありますか?

ありがとう、ポール

PS: ベクトルのサイズを変更する必要がないことを保証できる場合、この使用法は完全に問題なく (そして効率的) であることにも注意してください。

4

4 に答える 4

5

わかりました、virtualboxにWin8 + MSVC2012をインストールして試してみました。ねえ、Windows 8 はマウスが煩わしいし、ホバリングするだけでボタンを押す必要もないし、スクリーン イン ア ウィンドウでは難しい。

結果は興味深いものであり、まだ矛盾しています。

MSVC 2010: ecatmur が示唆したように、バグは移動セマンティクスに由来します。

問題は、 v.insert(v.end(),v[0]); insert(it, T && val) メソッドを選択しますが、これは 2 つの面で間違っています: 1) v[0] の破壊につながる可能性があります。const& 参照が保持され、移動ではなくコピーによって新しいバージョンが作成されることを示唆しています。2) ベクトルのサイズを変更する前に、コード パスが val のコピーを作成しません。

push_back(&&) 内の余分なコード (ハッキング?) により、問題がすぐに気付かなかったことに注意してください - MSVC2012 に関連する下部の詳細な解説を参照してください。

(insert(it,const&) は、ベクトルのサイズを変更する前に最初に新しい項目を正しくコピーすることに注意してください。したがって、正しい方法が選択されていれば、まったく問題はありませんでした)。

MSVC 2012 では、これは insert(it, const T & val) メソッドを正しく選択することで修正されますが、push_back() には誤った使用法を「修正」するための余分なコードがあることがわかります。

次のテストを検討してください。

#include <vector>
#include <string>

using std::vector;
using std::string;

int main()
{
   vector<string> vec1;
   vec1.push_back("hello");

   for (int i = 0; i != 1000; ++i)
   {
       string temp = vec1[0];
      vec1.push_back( std::move(vec1[0]) );
   }

   vector<string> vec2;
   vec2.push_back("hello");

   for (int i = 0; i != 1000; ++i)
   {
       string temp = vec2[0];
      vec2.insert( vec2.end(), std::move(vec2[0]) );
   }

   return 0;
}

どちらの場合も、 std::move() を使用して && move メソッドが選択されるように強制します。どちらの場合も、コードは大惨事を引き起こし、できればクラッシュするはずです。

ただし、MSVC 2012 では、_Val がベクターと同じアドレス空間にあるかどうかを検出する追加のコードが push_back(&&) に含まれているため、push_back() ループは正常に機能し、そうである場合は移動ではなくコピーを作成します。しかし、新しいアイテムが厳密には同じメモリ空間にあるのではなく、元のベクトルの一部 (pimpl ポインターなど) の場合はどうなるでしょうか? push_back(&&) を強制終了させる方法は想像できます。

確かに、これは実際には必要ありません。プログラマーが std::move() と言った場合、それが起こるはずですよね? 余分なチェックは、確か​​に不必要な CPU サイクルを使用しています。

insert() ループにはこのハックがありません。つまり、std::move() を誤って使用すると、時々破損するだけです。個人的には、クライアントにデモンストレーションをしているときにのみ失敗するよりも、高速フェイルを好みます。

だから...解決策...

  1. v.insert(v.end(), v[0]) などは使用しないでください。これは、サードパーティのコード (Boost、VTK、QT、tbb、xml ライブラリなど) が数百万行のコードのどこかで使用している可能性があるため、不合理な要件です。私が使用するサードパーティのライブラリはすべて再コンパイルするので、私のコードに問題があれば、それらも問題になります。

  2. MSVC 2012 RC にアップグレードします。ゴールドになるまで待つ必要があります。その後、期待どおりに動作します (他の部分に新しいエキサイティングなバグがあります)。

  3. ヘッダーをハックして使用状況を検出します。私はそれをしましたが、検出が機能するのはコードが実際に実行されたときだけです。

  4. ヘッダーをハックして、挿入 (&&) を修正します。(そして、すべてのライブラリ/プロジェクトを再コンパイルします-ため息)。最も簡単な方法は、単に insert(&&) バリアントをコメント アウトすることです (その後、C++11 以前のパフォーマンスに戻ります)。別のアプローチは、同じ push_back(&&) ハックを使用することですが、信頼できるアプローチとは思えません。おそらく、push_back(&&) もコメントアウトする必要があります。

さらに更新: ヘッダーを修正しました。シンプルになりました…

MSVC2010 の insert(&&) 宣言は次のようになります。

template<class _Valty>
iterator insert(const_iterator _Where, _Valty&& _Val)

MSVC2012 の insert(&&) はテンプレート部分を削除し、次のようになりました。

iterator insert(const_iterator _Where, _Ty&& _Val)

したがって、テンプレート化された _Valty を MSVC2010 の insert() から削除しただけで、正しいメソッドが選択されました。また、push_back(&&) の宣言方法にも一致するようになりました (つまり、パラメーターにテンプレートがありません)。emplace*(&&) メソッドのテンプレート化されたパラメーターはまだありますが、そこには const& の混乱はありません。

于 2012-07-26T06:33:21.217 に答える
2

編集:最初は、既存の要素を挿入すると未定義の動作になる可能性があるという印象を受けました。次の理由により、私はもはやそうであるとは信じていません。

重複した要素をベクターに挿入する方法は? 標準には、既存の要素への参照の挿入を禁止する言語はありません。イテレータと参照の無効化を参照する言語は、(他の指示がない場合) 操作が完了した後の動作を参照するものとしてのみ読み取ることができます。

オーバーラップされた vector::insert の動作に従って、イテレータ引数がinsert(it, first, last)シーケンスへのイテレータになってはならないことが指定されていることに注意してください。そのような言語がないことはpush_back、シーケンスへの参照が明確に許可されていることを意味します ( inclusio unius est exclusio alteriusの法的原則により)。

リンクしたバグ レポートを見ると、このケースでの MSVC のクラッシュは、C++11 移動セマンティクスの存在下でのコードの破損の結果であり、意図したものではなかったと思います。g++ は、既存の要素を次の場所にコピー/移動する前に、挿入された要素を新しく割り当てられたメモリの適切な場所にコピーすることで (私が思うに) このケースを処理します。

void insert(it, const T &t) {
    if (size() + 1 > capacity()) {
        T *new_data = (T *) malloc(sizeof(T) * capacity() * 2);
        new (&new_data[it - begin()]) T(t);
        // move [begin(), it) to [new_data, &new_data[it - begin()])
        // move [it, end()) to [&new_data[it - begin() + 1], &new_data[size() + 1])
    }
    ...
}

ヘッダーをハッキングする代わりstd::vectorに、独自のクラス テンプレートでラップすることができます。標準の実装を変更する場合は、再割り当てが発生しないように注意するコードを壊さないように注意してください。

v.reserve(v.size() + 1);
v.push_back(v[0]);
于 2012-07-25T15:39:22.937 に答える
1

4.4 の実装を見るとpush_backinsertバッファを拡張するバッファ呼び出し_M_insert_auxを拡張する必要がある場合は、最初に新しい要素をコピーします (つまり、この時点では元のオブジェクトは変更されていないため、エイリアシングは問題になりません)。次に、以前に存在したすべての要素。したがって、実装は問題ありません。

標準の一部として、エイリアシングに関する制限はありません。そのため、コードは準拠しており、未定義の動作が発生することはありません。

于 2012-07-25T16:27:26.843 に答える
1

ここで私自身の質問に答えると、

私のコードとほぼ同じバグ レポートを見つけました: http://connect.microsoft.com/VisualStudio/feedback/details/735732

上記のコメントで報告されているように、MSVC 2012 で修正されたようです。

GCC コードを詳しく調べたところ、関連する可能性があることがここで言及されています。既存のベクトルに。これは、呼び出し元 00329 // const lvalue ref によって要素を取得する場合のみの問題です (23.1/13 を参照)。

しかし、#ifdef が多すぎて、それが何をしているのか正確に把握できません。

したがって、答えは MSVC 2012 にアップグレードするか、少なくともヘッダーをハックして、他のどこに注意する必要があるかを知ることだと思います。

于 2012-07-25T15:49:49.420 に答える