反復中のstd :: setからの要素の削除


147

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

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

#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ループでの増分が未定義の動作になると考えました。とはいえ、私はこのテストコードを実行してすべてがうまくいき、理由を説明できません。

私の質問: これは標準セットの定義された動作ですか、この実装固有ですか?ちなみに、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か所だけしかインクリメントされないため、コードのエラーが発生しにくく、読みやすくなるので、このコードの方が好きです。



3
実際、私は質問する前にこの質問(およびその他の質問)を読みましたが、それらは他のSTLコンテナーに関連しており、私の最初のテストは明らかに機能していたため、両者には多少の違いがあると思いました。Mattの答えが出て初めて、valgrindを使用することを考えました。とはいえ、イテレータを1か所だけインクリメントすることでエラーの可能性を減らすため、他のソリューションよりも新しいソリューションを優先します。助けてくれてありがとう!
pedromanoel 2010

1
@pedromanoel ++itit++、イテレータの非表示の一時的なコピーを使用する必要がないため、よりも効率的です。Kornelのバージョンでは、フィルタリングされていない要素が最も効率的に反復されることが保証されています。
アルニタク

@Alnitak考えたことはありませんが、パフォーマンスの違いはそれほど大きくないと思います。コピーは彼のバージョンでも作成されますが、一致する要素に対してのみです。したがって、最適化の程度は、セットの構造に完全に依存しています。かなり長い間、コードを事前に最適化していたため、プロセスの可読性とコーディング速度が損なわれていました...したがって、他の方法を使用する前に、いくつかのテストを実行します。
pedromanoel

回答:


178

これは実装に依存します:

標準23.1.2.8:

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

多分あなたはこれを試すことができます-これは標準に準拠しています:

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

it ++はpostfixであるため、古い位置を渡して消去することに注意してください。ただし、演​​算子により、最初は新しい位置にジャンプします。

2015.10.27の更新: 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;
    }
}

2
これはdeque MSVC2013 では機能しません。それらの実装にバグがあるか、これが機能しないようにするための要件が​​もう1つありますdeque。STL仕様は複雑すぎて、すべての実装がそれに従うと期待することはできません。もちろん、カジュアルなプログラマーがそれを覚えておくことはできません。STLは飼いならしを超えた怪物であり、ユニークな実装がない場合(そしてテストスイートがある場合は、ループ内の要素を削除するなどの明らかなケースを明らかにカバーしていないため)、STLは光沢のあるもろいおもちゃになります横から見るとバツグン。
kuroi neko 2015

@MatthieuM。C ++ 11ではそうです。C ++ 17では、イテレータ(C ++ 11ではconst_iterator)が必要です。
tartaruga_casco_mole

18

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

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

重要な条件であり、スコープ内の初期化や操作後の操作が不要な場合は、whileループを使用することをお勧めします。すなわちfor ( ; it != numbers.end(); )while (it != numbers.end())
iammilind 2018

7

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

何かのクラッシュすることなく実行して振る舞うが、あなたはそれが期待通りならば、それはありません、それは未定義の動作ではないことを証明。それが証明するのは、その特定のオペレーティングシステムで特定のコンパイラーを使用してコンパイルした後、その特定の実行に対してその動作が偶然に観察されたことである。

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


ああ、私は未定義の動作が「それは私にとってはうまくいくが、すべての人にとってはうまくいかない」ことを意味する可能性があることをよく知っています。この動作が正しいかどうかわからなかったので、私がこの質問をしたのはそのためです。もしそうなら、私はそのままにしておくよりも。whileループを使用すると、問題が解決しますか?提案した解決策で質問を編集しました。チェックアウトしてください。
pedromanoel

それも私にとってはうまくいきます。しかし、条件をに変更するif (n > 2 && n < 7 )と、0 1 2 4 7 8 9が返されます。-ここでの特定の結果は、月の位相ではなく、消去方法と設定反復子の実装の詳細に依存する可能性があります(その位相ではありません)。実装の詳細に依存する必要があります)。;)
UncleBens 2010年

1
STLは、「未定義の動作」に多くの新しい意味を追加します。たとえば、「Microsoft std::set::eraseはイテレータを返すことができるようにすることで仕様を強化すると賢く考えたので、gccでコンパイルするとMSVCコードが強打されます」、または「Microsoftはバインドチェックを行うstd::bitset::operator[]ので、慎重に最適化されたビットセットアルゴリズムはMSVCでコンパイルするとクロールされます。」STLには独自の実装はなく、その仕様は指数関数的に増大する肥大化した混乱であるため、ループ内から要素を削除するには上級プログラマーの専門知識が必要になるのも不思議ではありません...
kuroi neko

2

警告するだけですが、dequeコンテナの場合、dequeイテレータがnumbers.end()と等しいかどうかをチェックするすべてのソリューションは、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;
      }
    }
  }
}

鍵はdo not trust an old remembered dq.end() value, always compare to a new call to dq.end()
Jesse Chisholm

2

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

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

そしてそれはのために働くだろうvectorsetdeque、などを参照してくださいcppReference詳細は。


1

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


1
Set<T>::eraseバージョンはイテレータを返しません。
Arkaitz Jimenez、

4
実際にはそうですが、MSVC実装でのみです。したがって、これは本当に実装固有の答えです。:)
ユージーン

1
@Eugene C ++ 11でのすべての実装に対してそれを行います
mastov '22

gcc 4.8withの一部の実装にはc++1y、消去にバグがあります。 it = collection.erase(it);は動作するはずですが、使用する方が安全かもしれませんcollection.erase(it++);
ジェシーチザム

1

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

このソリューションでは効率が低下する可能性があります。

ベクトルや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'メソッドが呼び出されます。


0

私は同じ古い問題に遭遇し、上記の解決策の意味で、以下のコードがより理解しやすいとわかりました。

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();
}

これは、常にすべてのアイテムを消去する場合にのみ機能します。OPは、アイテムを選択的に消去し、有効なイテレータを保持します。
ジェシーチザム
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.