179

セットを調べて、定義済みの基準を満たす要素を削除する必要があります。

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

#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 か所でのみインクリメントされ、コードがエラーを起こしにくくなり、読みやすくなるため、このコードの方が気に入っています。

4

8 に答える 8

211

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

標準 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 更新: 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;
    }
}
于 2010-05-20T14:13:56.913 に答える
22

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;                                                               
    }                                                                       
} 
于 2010-05-20T14:35:05.043 に答える
8

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

何かがクラッシュせずに実行され、期待どおりに動作する場合、それが未定義の動作ではないことを証明するものではありません。それが証明するのは、その特定のオペレーティングシステムでその特定のコンパイラをコンパイルした後、その動作がその特定の実行で観察されたとおりであったことだけです。

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

于 2010-05-20T14:15:38.757 に答える
2

deque コンテナーの場合、deque イテレーターが numbers.end() と等しいかどうかをチェックするすべてのソリューションは、gcc 4.8.4 で失敗する可能性が高いことに注意してください。つまり、deque の要素を消去すると、通常は 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()

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

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;
      }
    }
  }
}
于 2015-08-23T02:32:24.757 に答える
1

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

于 2010-05-20T14:09:49.603 に答える