3

バックグラウンド

しばらく前に、非常に奇妙で一見間違っていると思われる動作に遭遇したため、GCC にバグ レポートを提出しました。レポートと私が得た回答は次の場所で確認できます。

http://gcc.gnu.org/bugzilla/show_bug.cgi?id=47305

(ここではそのほとんどを複製します。)

当時、私は答えを理解していませんでしたが、StackOverflow のメンバーではなく、それについて尋ねる人もいなかったので、回避策をハックして続行しました。しかし最近、私はこのコードを再検討していましたが、これがバグではない理由をまだ理解していないので...

私の質問

私の Mac (現在は OS X、Darwin 12.2.0 x86_64) に含まれている C++ stdlib ディストリビューションでは、106 ~ 116 行目の実装std::vector::erase()が次の/usr/include/c++/4.2.1/vector.tccように示されています。

template<typename _Tp, typename _Alloc>
  typename vector<_Tp, _Alloc>::iterator
  vector<_Tp, _Alloc>::
  erase(iterator __position)
  {
    if (__position + 1 != end())
      std::copy(__position + 1, end(), __position);
    --this->_M_impl._M_finish;
    this->_M_impl.destroy(this->_M_impl._M_finish);
    return __position;
  }

が指す要素に対して呼び出されるのではなく、this の呼び出しの前にベクトル内の最後destroy()の要素に対して呼び出されることに注意してください。これは間違っていると思います。代わりに、 が指す要素を呼び出す必要があると思います。単純な POD 型の場合、これはそれほど大きな問題ではありませんが、デストラクタに副作用があるクラス (スマート ポインターなど) の場合は、重要になる可能性があります。erase()__positiondestroy()__position

次のコードは、問題を示しています。

#include <vector>
#include <iostream>

class MyClass
{
    int m_x;
public:
     MyClass(int x) : m_x(x) { }
    ~MyClass()
    {
        std::cerr << "Destroying with m_x=" << m_x << std::endl;
    }
};

int main(void)
{
    std::vector<MyClass> testvect;
    testvect.reserve(8);
    testvect.push_back(MyClass(1));
    testvect.push_back(MyClass(2));
    testvect.push_back(MyClass(3));
    testvect.push_back(MyClass(4));
    testvect.push_back(MyClass(5));

    std::cerr << "ABOUT TO DELETE #3:" << std::endl;

    testvect.erase(testvect.begin() + 2);

    std::cerr << "DONE WITH DELETE." << std::endl;

    return 0;
}

これを Mac で g++ バージョン 4.2.1 (コマンド ライン引数なし) でコンパイルすると、実行すると次のようになります。

Destroying with m_x=1
Destroying with m_x=2
Destroying with m_x=3
Destroying with m_x=4
Destroying with m_x=5
ABOUT TO DELETE #3:
Destroying with m_x=5
DONE WITH DELETE.
Destroying with m_x=1
Destroying with m_x=2
Destroying with m_x=4
Destroying with m_x=5

「ABOUT TO DELETE #3」メッセージの後の重要な行は、私が追加した 5 番目のもの (のコピー) のためにデストラクタが実際に呼び出されたことを示していることに注意してください。 重要なことに、#3 のデストラクタは呼び出されません!!

範囲を取るバージョンerase()(2 つの反復子) にも同様の問題があるようです。

だから私の質問は、ベクトルから消去している要素のデストラクタが呼び出されると期待するのは間違っているのでしょうか? これを当てにできないと、ベクトルでスマート ポインターを安全に使用できないようです。それとも、これは Apple が配布する STL ベクトル実装の単なるバグですか? 明らかな何かが欠けていますか?

4

4 に答える 4

4

3eraseを含む要素を使用する場合、空白を埋めるために次の要素をシフトバックする必要があります。次に、要素#3には #4 の要素が割り当てられ、要素# 4には#5の要素が割り当てられます。最後の要素#5は、いずれにせよ削除されようとしているため、その値がそのまま残されます。

vector範囲外になると、残りの 4 つの要素が破棄されていることがわかります。

でスマート ポインターを保持していvectorた場合、代入演算子が呼び出されたときにリソースが適切に解放されます。

于 2012-10-19T21:32:11.590 に答える
3

実際、問題はありません。ラインで

std::copy(__position + 1, end(), __position);

削除された要素は、連続する要素で上書きされます。解放する必要があるリソースを保持している場合は、operator=.

C++11 では、コピーではなく移動を使用する必要があります。しかし、あなたが投稿したのは、 の OK C++03 実装ですstd::vector::erase

于 2012-10-19T21:32:24.577 に答える
2

デストラクタは最後の要素に対してのみ呼び出されますが、消去されるオブジェクトは次の要素から代入されることによって上書きされます。そのため、代入演算子は古いリソースを解放します。型がスマート ポインターの場合は、参照を調整し、必要に応じて制御対象を削除することを意味します。

于 2012-10-19T21:33:45.467 に答える
2

これは妥当な点です。実装する方法が少なくとも 2 つありますerase

  • 要素 3 を破棄し、次に 4 から要素 3 をコピー構築し、次に 5 から 4 をコピー構築し、次に 5 を破棄します。
  • 4から3に、次に5から4にコピーアサインし、5を破壊します。

C++11 では、それを行う 3 番目の方法が導入されています。

  • 4から3に、次に5から4に移動割り当てし、5を破壊します。

実際vector::erase、最初の方法は、23.2.4.3/4 の C++03 標準で禁止されています。

複雑さ: T のデストラクタは、消去された要素の数だけ呼び出されますが、T の代入演算子は、消去された要素の後にベクトルの要素の数だけ呼び出されます。

このテキストは、主に操作の実行時の複雑さを示すように設計されていますが、2 番目の実装が必須であることがわかります。C++11 は、「割り当て」の代わりに「移動割り当て」を使用して同じことを言います。

最初の方法には、より根本的な問題もあります。それは、一般に (ただし、どちらにもint当てはまらないためMyClass)、コピーが失敗する可能性があることです。ベクトルeraseの 3 番目の要素が破棄され、4 番目の要素からのコピーが失敗した場合、ベクトルはかなり危険な状態になります。3 番目の要素は適切なオブジェクトではなくなります。したがって、標準の制限はランタイムを定義するだけではなく、この悪い失敗のケースを防ぎます。

于 2012-10-19T21:49:58.190 に答える