web-dev-qa-db-ja.com

反復中にSTLセットから要素を削除する

セットを調べて、事前定義された基準を満たす要素を削除する必要があります。

これは私が書いたテストコードです。

#include <set>
#include <algorithm>

void printElement(int value) {
    std::cout << value << " ";
}

int main() {
    int initNum[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    std::set<int> numbers(initNum, initNum + 10);
    // print '0 1 2 3 4 5 6 7 8 9'
    std::for_each(numbers.begin(), numbers.end(), printElement);

    std::set<int>::iterator it = numbers.begin();

    // iterate through the set and erase all even numbers
    for (; it != numbers.end(); ++it) {
        int n = *it;
        if (n % 2 == 0) {
            // wouldn't invalidate the iterator?
            numbers.erase(it);
        }
    }

    // print '1 3 5 7 9'
    std::for_each(numbers.begin(), numbers.end(), printElement);

    return 0;
}

最初は、セットを反復しながら要素をセットから消去するとイテレータが無効になり、forループでのインクリメントに未定義の動作が生じると考えました。とはいえ、私はこのテストコードを実行し、すべてうまくいったので、その理由を説明できません。

私の質問:これはstdセットに対して定義された動作ですか、またはこの実装固有ですか?ちなみに、ubuntu 10.04(32ビット版)でgcc 4.3.3を使用しています。

ありがとう!

提案されたソリューション:

これは、セットから要素を繰り返し消去する正しい方法ですか?

while(it != numbers.end()) {
    int n = *it;
    if (n % 2 == 0) {
        // post-increment operator returns a copy, then increment
        numbers.erase(it++);
    } else {
        // pre-increment operator increments, then return
        ++it;
    }
}

編集:推奨ソリューション

私は、まったく同じように機能しますが、よりエレガントに思えるソリューションを見つけました。

while(it != numbers.end()) {
    // copy the current iterator then increment it
    std::set<int>::iterator current = it++;
    int n = *current;
    if (n % 2 == 0) {
        // don't invalidate iterator it, because it is already
        // pointing to the next element
        numbers.erase(current);
    }
}

While内に複数のテスト条件がある場合、各テスト条件でイテレータをインクリメントする必要があります。イテレータがインクリメントされる1か所のみであるため、このコードのほうが好きです。

130
pedromanoel

これは実装依存です:

標準23.1.2.8:

挿入メンバーは、イテレーターとコンテナーへの参照の有効性に影響を与えず、消去メンバーは、イテレーターと消去された要素への参照のみを無効にします。

たぶんこれを試すことができます-これは標準に準拠しています:

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        numbers.erase(it++);
    }
    else {
        ++it;
    }
}

It ++は後置であるため、消去するために古い位置を渡しますが、最初に演算子により新しい位置にジャンプします。

2015.10.27 update: C++ 11はこの問題を解決しました。 iterator erase (const_iterator position);は、最後に削除された要素に続くイテレータを返します(最後の要素が削除された場合はset::end)。 C++ 11スタイルは次のとおりです。

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        it = numbers.erase(it);
    }
    else {
        ++it;
    }
}
158

Valgrindを介してプログラムを実行すると、多数の読み取りエラーが表示されます。言い換えれば、はい、イテレーターは無効にされていますが、あなたの例では幸運になっています(または未定義の動作の悪影響が見られないので、本当に不運です)。これに対する1つの解決策は、一時イテレーターを作成し、一時を増やし、ターゲットイテレーターを削除してから、ターゲットを一時に設定することです。たとえば、ループを次のように書き直します。

std::set<int>::iterator it = numbers.begin();                               
std::set<int>::iterator tmp;                                                

// iterate through the set and erase all even numbers                       
for ( ; it != numbers.end(); )                                              
{                                                                           
    int n = *it;                                                            
    if (n % 2 == 0)                                                         
    {                                                                       
        tmp = it;                                                           
        ++tmp;                                                              
        numbers.erase(it);                                                  
        it = tmp;                                                           
    }                                                                       
    else                                                                    
    {                                                                       
        ++it;                                                               
    }                                                                       
} 
18
Matt

「未定義の動作」の意味を誤解しています。未定義の動作は、「これを行うと、プログラムwillがクラッシュしたり、予期しない結果が生じる」という意味ではありません。コンパイラ、オペレーティングシステム、月の満ち欠けなどに応じて、「これを行うと、プログラムcouldクラッシュまたは予期しない結果が生じる」、または他の何かを行うことを意味します。

何かがクラッシュすることなく実行され、期待どおりに動作する場合、それは未定義の動作ではないことをnot証明します。証明されているのは、その特定のオペレーティングシステムで特定のコンパイラを使用してコンパイルした後、その特定の実行でその動作が発生したことです。

セットから要素を消去すると、消去された要素の反復子が無効になります。無効化されたイテレータの使用は未定義の動作です。観察された動作が、この特定のインスタンスで意図したとおりであることがたまたま起こりました。コードが正しいという意味ではありません。

7
Tyler McHenry

警告するために、dequeコンテナの場合、number.end()と等しいdequeイテレータをチェックするすべてのソリューションは、gcc 4.8.4で失敗する可能性が高いことを警告します。つまり、両端キューの要素を消去すると、通常、numbers.end()へのポインターが無効になります。

#include <iostream>
#include <deque>

using namespace std;
int main() 
{

  deque<int> numbers;

  numbers.Push_back(0);
  numbers.Push_back(1);
  numbers.Push_back(2);
  numbers.Push_back(3);
  //numbers.Push_back(4);

  deque<int>::iterator  it_end = numbers.end();

  for (deque<int>::iterator it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
      cout << "Erasing element: " << *it << "\n";
      numbers.erase(it++);
      if (it_end == numbers.end()) {
    cout << "it_end is still pointing to numbers.end()\n";
      } else {
    cout << "it_end is not anymore pointing to numbers.end()\n";
      }
    }
    else {
      cout << "Skipping element: " << *it << "\n";
      ++it;
    }
  }
}

出力:

Erasing element: 0
it_end is still pointing to numbers.end()
Skipping element: 1
Erasing element: 2
it_end is not anymore pointing to numbers.end()

この特定のケースでは両端キュー変換は正しいですが、途中でエンドポインタが無効になっていることに注意してください。異なるサイズの両端キューを使用すると、エラーがより明確になります。

int main() 
{

  deque<int> numbers;

  numbers.Push_back(0);
  numbers.Push_back(1);
  numbers.Push_back(2);
  numbers.Push_back(3);
  numbers.Push_back(4);

  deque<int>::iterator  it_end = numbers.end();

  for (deque<int>::iterator it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
      cout << "Erasing element: " << *it << "\n";
      numbers.erase(it++);
      if (it_end == numbers.end()) {
    cout << "it_end is still pointing to numbers.end()\n";
      } else {
    cout << "it_end is not anymore pointing to numbers.end()\n";
      }
    }
    else {
      cout << "Skipping element: " << *it << "\n";
      ++it;
    }
  }
}

出力:

Erasing element: 0
it_end is still pointing to numbers.end()
Skipping element: 1
Erasing element: 2
it_end is still pointing to numbers.end()
Skipping element: 3
Erasing element: 4
it_end is not anymore pointing to numbers.end()
Erasing element: 0
it_end is not anymore pointing to numbers.end()
Erasing element: 0
it_end is not anymore pointing to numbers.end()
...
Segmentation fault (core dumped)

これを修正する方法の1つを次に示します。

#include <iostream>
#include <deque>

using namespace std;
int main() 
{

  deque<int> numbers;
  bool done_iterating = false;

  numbers.Push_back(0);
  numbers.Push_back(1);
  numbers.Push_back(2);
  numbers.Push_back(3);
  numbers.Push_back(4);

  if (!numbers.empty()) {
    deque<int>::iterator it = numbers.begin();
    while (!done_iterating) {
      if (it + 1 == numbers.end()) {
    done_iterating = true;
      } 
      if (*it % 2 == 0) {
    cout << "Erasing element: " << *it << "\n";
      numbers.erase(it++);
      }
      else {
    cout << "Skipping element: " << *it << "\n";
    ++it;
      }
    }
  }
}
2
McKryak

STLメソッド 'remove_if'を使用すると、イテレータでラップされているオブジェクトを削除しようとしたときに、奇妙な問題を防ぐことができると思います。

このソリューションは効率が悪い場合があります。

Vectorやm_bulletsと呼ばれるリストのような、ある種のコンテナがあるとしましょう:

Bullet::Ptr is a shared_pr<Bullet>

'it'は 'remove_if'が返す反復子で、3番目の引数はコンテナのすべての要素で実行されるラムダ関数です。コンテナにはBullet::Ptrが含まれているため、ラムダ関数はその型(またはその型への参照)を引数として渡す必要があります。

 auto it = std::remove_if(m_bullets.begin(), m_bullets.end(), [](Bullet::Ptr bullet){
    // dead bullets need to be removed from the container
    if (!bullet->isAlive()) {
        // lambda function returns true, thus this element is 'removed'
        return true;
    }
    else{
        // in the other case, that the bullet is still alive and we can do
        // stuff with it, like rendering and what not.
        bullet->render(); // while checking, we do render work at the same time
        // then we could either do another check or directly say that we don't
        // want the bullet to be removed.
        return false;
    }
});
// The interesting part is, that all of those objects were not really
// completely removed, as the space of the deleted objects does still 
// exist and needs to be removed if you do not want to manually fill it later 
// on with any other objects.
// erase dead bullets
m_bullets.erase(it, m_bullets.end());

'remove_if'は、ラムダ関数がtrueを返したコンテナを削除し、そのコンテンツをコンテナの先頭に移動します。 'it'は、ガベージと見なされる未定義のオブジェクトを指します。 'it'からm_bullets.end()までのオブジェクトはメモリを占有するため消去できますが、ゴミが含まれているため、その範囲で 'erase'メソッドが呼び出されます。

1
John Behm

C++ 20には「均一なコンテナ消去」があり、次のように記述できます。

std::erase_if(numbers, [](int n){ return n % 2 == 0 });

vectorsetdequeなどでも機能します。詳細については、 cppReference を参照してください。

1
Marshall Clow

この動作は実装固有です。イテレータの正確性を保証するには、「it = numbers.erase(it);」を使用する必要があります要素を削除する必要がある場合はステートメント、他の場合は単純にイテレータを確認します。

1
Vitaly Bogdanov

私は同じ古い問題に遭遇し、以下のコードを見つけました理解可能これは上記のソリューションごとの方法です。

std::set<int*>::iterator beginIt = listOfInts.begin();
while(beginIt != listOfInts.end())
{
    // Use your member
    std::cout<<(*beginIt)<<std::endl;

    // delete the object
    delete (*beginIt);

    // erase item from vector
    listOfInts.erase(beginIt );

    // re-calculate the begin
    beginIt = listOfInts.begin();
}
0
Anurag