반복하는 동안 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 루프의 증가분에는 정의되지 않은 동작이 있다고 생각했습니다. 그럼에도 불구 하고이 테스트 코드를 실행했는데 모두 잘 진행되었으며 이유를 설명 할 수 없습니다.

내 질문 : 이것은 std 세트에 대해 정의 된 동작입니까, 아니면 구현에 특정한 것입니까? 그런데 우분투 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 안에 여러 테스트 조건이있는 경우 각 테스트 조건이 반복자를 증가시켜야합니다. 반복자가 한 곳에서만 증가하기 때문에이 코드가 더 좋습니다. 오류가 적고 읽기 쉽습니다.



3
사실, 나는 묻기 전에이 질문 (및 다른 것들)을 읽었지만 다른 STL 컨테이너와 관련이 있었고 초기 테스트가 효과가 있었기 때문에 그들 사이에 약간의 차이가 있다고 생각했습니다. Matt의 대답 후에 만 ​​valgrind를 사용하려고 생각했습니다. 그럼에도 불구하고 반복기를 한 곳에서만 증가시켜 오류 가능성을 줄이기 때문에 다른 솔루션보다 새로운 솔루션을 선호합니다. 도와 주셔서 감사합니다!
pedromanoel

1
@pedromanoel ++itit++iterator의 보이지 않는 임시 복사본을 사용할 필요 가 없기 때문에 다소 효율적 입니다. Kornel 버전은 더 오래 필터링되지 않은 요소가 가장 효율적으로 반복되도록합니다.
Alnitak

@Alnitak 나는 그것에 대해 생각하지 않았지만 성능의 차이가 그렇게 크지 않을 것이라고 생각합니다. 사본도 그의 버전으로 작성되지만 일치하는 요소에 대해서만 작성됩니다. 따라서 최적화 정도는 세트의 구조에 전적으로 의존합니다. 꽤 오랫동안 코드를 미리 최적화하여 프로세스의 가독성과 코딩 속도를 저하 시켰습니다. 따라서 다른 방법을 사용하기 전에 몇 가지 테스트를 수행했습니다.
pedromanoel

답변:


178

이것은 구현에 따라 다릅니다.

표준 23.1.2.8 :

삽입 부재는 이터레이터의 유효성과 컨테이너에 대한 참조에 영향을 미치지 않아야하며, 소거 부재는 소거 된 요소에 대한 반복자와 참조 만 무효화해야합니다.

아마도 당신은 이것을 시도 할 수 있습니다-이것은 표준 준수입니다.

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        numbers.erase(it++);
    }
    else {
        ++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;
    }
}

2
deque MSVC2013 에서는 작동하지 않습니다 . 그들의 구현이 버그가 있거나 이것의 작동을 방해하는 또 다른 요구 사항이 있습니다 deque. STL 스펙은 너무 복잡해서 모든 프로그래머가 그것을 암기하는 것은 물론 모든 구현이 그것을 따를 것으로 기대할 수는 없습니다. STL은 길들이기 어려운 괴물이며, 독특한 구현이 없으며 (테스트 스위트는 루프에서 요소를 삭제하는 것과 같은 명백한 경우를 다루지 않는 것 같습니다) STL을 빛나는 취성 완구 옆으로 볼 때 쾅.
kuroi neko

@MatthieuM. C ++ 11에서 수행됩니다. C ++ 17에서는 이제 iterator (C ++ 11의 const_iterator)를 사용합니다.
tartaruga_casco_mole

19

valgrind를 통해 프로그램을 실행하면 많은 읽기 오류가 표시됩니다. 다시 말해서, 반복자가 무효화되고 있지만 예제에서 운이 좋지 않습니다 (정의되지 않은 동작의 부정적인 영향을 보지 못해 운이 좋지 않습니다). 이에 대한 한 가지 해결책은 임시 반복자를 작성하고, 온도를 높이고, 대상 반복자를 삭제 한 다음 대상을 임시로 설정하는 것입니다. 예를 들어, 다음과 같이 루프를 다시 작성하십시오.

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

7

"정의되지 않은 동작"의 의미를 오해합니다. 정의되지 않은 동작은 "이 작업을 수행하면 프로그램 충돌하거나 예기치 않은 결과 발생 함"을 의미하지 않습니다 . "이 작업을 수행하면 프로그램 충돌하거나 예기치 않은 결과가 발생할 수 있습니다"또는 컴파일러, 운영 체제, 달의 위상 등에 따라 다른 작업을 수행 할 수 있습니다.

충돌없이 무언가가 실행되고 예상대로 작동 하는 경우 정의되지 않은 동작 이 아니라는 증거는 아닙니다. 모든 특정 운영 체제에서 특정 컴파일러로 컴파일 한 후 해당 특정 동작에 대해 해당 동작이 관찰 된 것으로 나타났습니다.

집합에서 요소를 지우면 이터레이터가 지워진 요소로 무효화됩니다. 무효화 된 반복자를 사용하는 것은 정의되지 않은 동작입니다. 관찰 된 행동이이 특정한 예에서 의도 한 것이 었습니다. 코드가 정확하다는 의미는 아닙니다.


아, 나는 정의되지 않은 행동이 "나에게는 효과가 있지만 모든 사람에게는 효과가 없다"는 것을 의미 할 수 있다는 것을 잘 알고있다. 이 행동이 올바른지 알 수 없기 때문에이 질문을 한 이유입니다. 그랬다면 그냥 그렇게 떠나는 것보다. while 루프를 사용하면 문제가 해결됩니까? 제안 된 솔루션으로 질문을 편집했습니다. 그것을 확인하시기 바랍니다.
pedromanoel

나에게도 효과가 있습니다. 그러나 조건을로 변경하면 if (n > 2 && n < 7 )0 1 2 4 7 8 9를 얻습니다 .-여기의 특정 결과는 아마도 달의 위상이 아니라 지우기 방법 및 반복기의 구현 세부 사항에 더 달려 있습니다. 구현 세부 사항에 의존해야합니다). ;)
UncleBens

1
STL은 "정의되지 않은 행동"에 많은 새로운 의미를 추가합니다. 예를 들어 "마이크로 소프트는 수 있도록하여 사양을 향상시키기 위해 스마트 생각 std::set::erase마이크로 소프트가 바운드를 확인하지", 또는 GCC로 컴파일 할 때 MSVC 코드는 강타와 함께 갈 것입니다, 그래서 반복자를 반환하는 " std::bitset::operator[]당신의주의 깊게 최적화 된 비트 세트의 알고리즘이 둔화 않도록를 MSVC로 컴파일 할 때 크롤링 " STL에는 고유 한 구현이 없으며 스펙이 기하 급수적으로 증가하는 엉망진창이므로 루프 내부에서 요소를 삭제하려면 고위 프로그래머의 전문 지식이 필요합니다.
kuroi neko

2

deque 컨테이너의 경우 deque iterator가 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 변환이 올바른 반면 종료 포인터는 그 과정에서 무효화되었습니다. 다른 크기의 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)

이 문제를 해결하는 방법 중 하나는 다음과 같습니다.

#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에는 "uniform container erasure"가 있으며 다음과 같이 작성할 수 있습니다.

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

그리고 그것은을 위해 작동합니다 vector, set, deque, 등을 참조 cppReference 대한 추가 정보를 원하시면.


1

이 동작은 구현에 따라 다릅니다. 반복자의 정확성을 보장하려면 "it = numbers.erase (it);"를 사용해야합니다. 요소를 삭제하고 다른 경우 단순히 반복자를 삽입 해야하는 경우 문.


1
Set<T>::eraseversion은 반복자를 반환하지 않습니다.
Arkaitz Jimenez

4
실제로는 가능하지만 MSVC 구현에서만 가능합니다. 따라서 이것은 실제로 구현에 대한 답변입니다. :)
Eugene

1
그것은 C와 모든 구현 ++ 11을 수행 @Eugene
mastov

일부 구현 gcc 4.8와 함께 c++1y삭제에 버그가 있습니다. it = collection.erase(it);작동해야하지만 사용하는 것이 더 안전 할 수 있습니다.collection.erase(it++);
Jesse Chisholm

1

STL 메소드 ' remove_if'를 사용 하면 반복자가 랩핑 한 객체를 삭제하려고 할 때 이상한 문제를 방지하는 데 도움이 될 수 있다고 생각 합니다.

이 솔루션은 효율성이 떨어질 수 있습니다.

벡터 또는 m_bullets라는 목록과 같은 컨테이너가 있다고 가정 해 봅시다.

Bullet::Ptr is a shared_pr<Bullet>

' it'는 ' '이 remove_if반환 하는 반복자 이며, 세 번째 인수는 컨테이너의 모든 요소에서 실행되는 람다 함수입니다. 컨테이너에가 포함되어 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'는 lambda 함수가 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는 항목을 선택적으로 지우고 여전히 유효한 반복자를 가지고 있습니다.
Jesse Chisholm 21
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.