벡터에서 항목을 제거하는 동안 C ++ 11 범위 'for'루프에서?


98

나는 IInventory *의 벡터를 가지고 있으며, 각각에 대해 작업을 수행하기 위해 C ++ 11 범위를 사용하여 목록을 반복하고 있습니다.

하나를 가지고 몇 가지 작업을 한 후 목록에서 제거하고 개체를 삭제하고 싶을 수 있습니다. delete포인터를 정리하기 위해 언제든지 호출 할 수 있다는 것을 알고 있지만 범위 for루프 에있는 동안 벡터에서 포인터를 제거하는 적절한 방법은 무엇 입니까? 그리고 목록에서 제거하면 루프가 무효화됩니까?

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

for (IInventory* index : inv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
}

8
멋지게 만들고 싶다면 std::remove_if"일을하는"술어와 함께 사용할 수 있으며 요소를 제거하려면 true를 반환합니다.
Benjamin Lindley 2012

인덱스 카운터를 추가하고 inv.erase (index)와 같은 것을 사용할 수없는 이유가 있습니까?
TomJ

4
@TomJ : 그것은 여전히 ​​반복을 망칠 것입니다.
Ben Voigt 2012

@TomJ와 그것은 성능 킬러가 될 것입니다. 지울 때마다 모든 요소를 ​​지운 후에 모든 요소를 ​​이동할 수 있습니다.
이반

1
@BenVoigt 나는 전환 권장 std::list이하
bobobobo

답변:


95

아니, 할 수 없습니다. 범위 기반 for은 컨테이너의 각 요소에 한 번 액세스해야하는 경우를위한 것입니다.

진행 for하면서 컨테이너를 수정하고, 요소에 두 번 이상 액세스하거나, 그렇지 않으면 컨테이너를 통해 비선형 방식으로 반복해야하는 경우 일반 루프 또는 사촌 중 하나를 사용해야합니다 .

예를 들면 :

auto i = std::begin(inv);

while (i != std::end(inv)) {
    // Do some stuff
    if (blah)
        i = inv.erase(i);
    else
        ++i;
}

5
여기서 적용 할 수있는 관용구를 지우고 제거하지 않겠습니까?
Naveen 2012

4
@Naveen 나는 분명히 그가 모든 항목을 반복해야하기 때문에, 그것으로 계산을 그렇게하려고하고하지 않기로 결정 가능성이 컨테이너에서 제거합니다. Erase-remove는 술어가 true, AFAIU를 리턴하는 요소를 지우고 반복 논리를 술어와 혼합하지 않는 것이 더 좋습니다.
Seth Carnegie

4
@SethCarnegie Erase-remove with a lambda for the predicate 우아하게 허용합니다 (이미 C ++ 11이기 때문에)
Potatoswatter

11
이 솔루션이 마음에 들지 않습니다. 대부분의 컨테이너에서 O (N ^ 2)입니다. remove_if더 나은.
Ben Voigt 2013

6
이 대답 정확 erase하고 새로운 유효한 반복자를 반환합니다. 효율적이지 않을 수 있지만 작동하는 것은 보장됩니다.
sp2danny

56

벡터에서 요소가 제거 될 때마다 지워진 요소 다음에 오는 각 요소가 이동되기 때문에 지워진 요소 또는 그 이후의 반복기가 더 이상 유효하지 않다고 가정해야합니다.

범위 기반 for 루프는 반복자를 사용하는 "일반"루프의 구문 설탕이므로 위의 내용이 적용됩니다.

즉, 간단하게 다음을 수행 할 수 있습니다.

inv.erase(
    std::remove_if(
        inv.begin(),
        inv.end(),
        [](IInventory* element) -> bool {
            // Do "some stuff", then return true if element should be removed.
            return true;
        }
    ),
    inv.end()
);

6
" 벡터가 요소를 유지하는 메모리 블록을 재 할당 할 가능성이 있기 때문입니다. "아니오, vector는를 호출하기 때문에 절대 재 할당되지 않습니다 erase. 반복기가 무효화되는 이유는 지워진 요소 다음의 각 요소가 이동되기 때문입니다.
ildjarn

2
캡처 기본값은 [&]그가 지역 변수로 "일부 작업을 수행"할 수 있도록하는 것이 적절합니다.
Potatoswatter 2011

2
이 외에도 당신은 당신의 서라운드 기억해야 반복자 기반 루프보다 간단을 보이지 않는 remove_if.erase, 그렇지 않으면 아무 일도 일어나지 않는다.
bobobobo

3
@bobobobo "반복자 기반 루프"가 Seth Carnegie의 대답 을 의미한다면 평균적 으로 O (n ^ 2)입니다. std::remove_ifO (n)입니다.
Branko Dimitrijevic

1
@bobobobo 실제로 임의 액세스 가 필요한 경우가 아니면 .
Branko Dimitrijevic

16

벡터를 반복하는 동안 이상적으로는 벡터를 수정하지 않아야합니다. 지우기-제거 관용구를 사용하십시오. 그렇게하면 몇 가지 문제가 발생할 수 있습니다. A의 이후 무효화 모든 반복자는 요소로 시작하는 개까지 삭제되는 것을 사용하여 반복자는 유효 있는지 확인해야합니다 :vectoreraseend()

for (MyVector::iterator b = v.begin(); b != v.end();) { 
    if (foo) {
       b = v.erase( b ); // reseat iterator to a valid value post-erase
    else {
       ++b;
    }
}

b != v.end()테스트는있는 그대로 필요합니다 . 다음과 같이 최적화하려는 경우 :

for (MyVector::iterator b = v.begin(), e = v.end(); b != e;)

e첫 번째 erase호출 후 무효화 되기 때문에 UB에 부딪 힐 것 입니다 .


@ildjarn : 네, 사실입니다! 오타였습니다.
부지런히

2
이것은 지우기-제거 관용구가 아닙니다. 에 대한 호출이 없으며 std::removeO (N)이 아닌 O (N ^ 2)입니다.
Potatoswatter

1
@Potatoswatter : 물론 아닙니다. 반복하면서 삭제 문제를 지적하려고했습니다. 내 문구가 소집을 통과하지 못한 것 같아요?
부지런히

5

해당 루프에있는 동안 요소를 제거하는 것이 엄격한 요구 사항입니까? 그렇지 않으면 삭제하려는 포인터를 NULL로 설정하고 모든 NULL 포인터를 제거하기 위해 벡터 위에 또 다른 패스를 만들 수 있습니다.

std::vector<IInventory*> inv;
inv.push_back( new Foo() );
inv.push_back( new Bar() );

for ( IInventory* &index : inv )
{
    // do some stuff
    // ok I decided I need to remove this object from inv...?
    if (do_delete_index)
    {
        delete index;
        index = NULL;
    }
}
std::remove(inv.begin(), inv.end(), NULL);

1

necroposting에 대해 미안하고 내 C ++ 전문 지식이 내 대답을 방해한다면 미안하지만 각 항목을 반복하고 가능한 변경 (예 : 인덱스 지우기)을 시도하는 경우 백 워드 for 루프를 사용해보십시오.

for(int x=vector.getsize(); x>0; x--){

//do stuff
//erase index x

}

인덱스 x를 지울 때 다음 루프는 마지막 반복 "앞에있는"항목에 대한 것입니다. 나는 이것이 누군가를 도왔기를 정말로 바란다.


x를 사용하여 특정 인덱스에 액세스 할 때 잊지 마세요. x-1 lol
lilbigwill99

1

OK, 어쨌든 늦었 어,하지만 : 죄송합니다, 정확하지 내가 지금까지 읽은 내용 - 그것은 이다 가능하면이 두 반복자가 필요합니다 :

std::vector<IInventory*>::iterator current = inv.begin();
for (IInventory* index : inv)
{
    if(/* ... */)
    {
        delete index;
    }
    else
    {
        *current++ = index;
    }
}
inv.erase(current, inv.end());

반복자가 가리키는 값을 수정하는 것만으로는 다른 반복기가 무효화되지 않으므로 걱정할 필요없이이 작업을 수행 할 수 있습니다. 실제로 std::remove_if(적어도 gcc 구현)은 매우 유사한 작업을 수행합니다 (클래식 루프 사용 ...). 아무것도 삭제하지 않고 지우지 않습니다.

그러나 이것은 스레드로부터 안전하지 않습니다 (!)-그러나 위의 다른 솔루션에도 적용됩니다.


이런 젠장. 이것은 과잉입니다.
Kesse

@Kesse 정말? 이것은 벡터로 얻을 수있는 가장 효율적인 알고리즘입니다 (범위 기반 루프인지 클래식 반복자 루프인지는 중요하지 않음) : 이렇게하면 벡터의 각 요소를 최대 한 번 이동하고 전체 벡터를 정확히 한 번 반복합니다. erase(물론 하나 이상의 단일 요소 를 삭제 한 경우) 각 일치 요소를 삭제하면 후속 요소를 몇 번 이동하고 벡터를 반복 합니까?
Aconcagua

1

예를 들어, 아래 예제는 벡터에서 홀수 요소를 제거합니다.

void test_del_vector(){
    std::vector<int> vecInt{0, 1, 2, 3, 4, 5};

    //method 1
    for(auto it = vecInt.begin();it != vecInt.end();){
        if(*it % 2){// remove all the odds
            it = vecInt.erase(it);
        } else{
            ++it;
        }
    }

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

    // recreate vecInt, and use method 2
    vecInt = {0, 1, 2, 3, 4, 5};
    //method 2
    for(auto it=std::begin(vecInt);it!=std::end(vecInt);){
        if (*it % 2){
            it = vecInt.erase(it);
        }else{
            ++it;
        }
    }

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

    // recreate vecInt, and use method 3
    vecInt = {0, 1, 2, 3, 4, 5};
    //method 3
    vecInt.erase(std::remove_if(vecInt.begin(), vecInt.end(),
                 [](const int a){return a % 2;}),
                 vecInt.end());

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;

}

아래 출력 aw :

024
024
024

이 메서드 erase는 전달 된 이터레이터의 다음 이터레이터를 반환합니다.

에서 여기 , 우리는 더 생성하는 방법을 사용할 수 있습니다 :

template<class Container, class F>
void erase_where(Container& c, F&& f)
{
    c.erase(std::remove_if(c.begin(), c.end(),std::forward<F>(f)),
            c.end());
}

void test_del_vector(){
    std::vector<int> vecInt{0, 1, 2, 3, 4, 5};
    //method 4
    auto is_odd = [](int x){return x % 2;};
    erase_where(vecInt, is_odd);

    // output all the remaining elements
    for(auto const& it:vecInt)std::cout<<it;
    std::cout<<std::endl;    
}

사용 방법을 보려면 여기를 참조하십시오 std::remove_if. https://en.cppreference.com/w/cpp/algorithm/remove


1

이 스레드 제목과는 반대로 두 가지 패스를 사용합니다.

#include <algorithm>
#include <vector>

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

std::vector<IInventory*> toDelete;

for (IInventory* index : inv)
{
    // Do some stuff
    if (deleteConditionTrue)
    {
        toDelete.push_back(index);
    }
}

for (IInventory* index : toDelete)
{
    inv.erase(std::remove(inv.begin(), inv.end(), index), inv.end());
}

0

훨씬 더 우아한 해결책은로 전환하는 것입니다 std::list(빠른 임의 액세스가 필요하지 않다고 가정).

list<Widget*> widgets ; // create and use this..

그런 다음 with .remove_if및 C ++ functor를 한 줄로 삭제할 수 있습니다 .

widgets.remove_if( []( Widget*w ){ return w->isExpired() ; } ) ;

그래서 여기서 저는 하나의 인수 (the Widget*) 를 받아들이는 펑터를 작성하고 있습니다. 반환 값은 Widget*목록에서 를 제거하는 조건입니다 .

이 구문이 맛있다고 생각합니다. 나는 내가 사용하는 것이 생각하지 않습니다 remove_if에 대한 표준 : : 벡터 -이 너무 많이 inv.begin()inv.end()노이즈가 사용 떨어져 아마 더 좋을 것 같아 정수 인덱스 기반의 삭제 또는 그냥 평범한 일반 오래된 반복자 기반 같이 (삭제 이하). 그러나 std::vector어쨌든 중간에서 제거하지 않아야 하므로이 list경우 목록 중간 삭제가 자주 발생하는 경우 a 로 전환하는 것이 좋습니다.

참고 그러나 내가 전화를 할 수있는 기회하지 않았다 delete상의 Widget*제거 '들. 이를 위해 다음과 같이 표시됩니다.

widgets.remove_if( []( Widget*w ){
  bool exp = w->isExpired() ;
  if( exp )  delete w ;       // delete the widget if it was expired
  return exp ;                // remove from widgets list if it was expired
} ) ;

다음과 같이 일반 반복기 기반 루프를 사용할 수도 있습니다.

//                                                              NO INCREMENT v
for( list<Widget*>::iterator iter = widgets.begin() ; iter != widgets.end() ; )
{
  if( (*iter)->isExpired() )
  {
    delete( *iter ) ;
    iter = widgets.erase( iter ) ; // _advances_ iter, so this loop is not infinite
  }
  else
    ++iter ;
}

의 길이가 마음에 들지 않으면 for( list<Widget*>::iterator iter = widgets.begin() ; ...사용할 수 있습니다.

for( auto iter = widgets.begin() ; ...

1
나는 당신 remove_ifstd::vector실제로 어떻게 작업하는지, 그리고 그것이 어떻게 복잡성을 O (N)으로 유지하는지 이해하지 못한다고 생각합니다 .
Ben Voigt

그건 중요하지 않습니다. a의 중간에서 제거하는 std::vector것은 항상 제거한 요소 이후에 각 요소를 슬라이드 std::list하므로 훨씬 더 나은 선택이됩니다.
bobobobo 2013

1
아니요, "각 요소를 위로 슬라이드"하지 않습니다. remove_if여유 공간 수만큼 각 요소를 위로 이동합니다. 그 때까지는 당신은 캐시 사용에 대한 계정 remove_ifA의 std::vectorA로부터 가능성이 성능이 뛰어 제거 std::list. 그리고 O(1)랜덤 액세스를 유지 합니다.
Ben Voigt 2013

1
그럼 당신은 질문을 찾아서 훌륭한 대답을 얻었습니다. 이 질문 은 두 컨테이너 모두에 대해 O (N) 인 목록 반복에 대해 설명합니다. 두 컨테이너 모두 O (N) 인 O (N) 요소를 제거합니다.
Ben Voigt 2013

2
사전 표시는 필요하지 않습니다. 한 번의 패스로이 작업을 완벽하게 수행 할 수 있습니다. "검사 할 다음 요소"와 "채울 다음 슬롯"을 추적하기 만하면됩니다. 술어를 기반으로 필터링 된 목록의 사본을 작성하는 것으로 생각하십시오. 술어가 true를 리턴하면 요소를 건너 뛰고 그렇지 않으면 복사하십시오. 그러나 목록 복사가 제자리에 만들어지고 복사 대신 스왑 / 이동이 사용됩니다.
Ben Voigt 2013

0

나는 다음을 할 것이라고 생각한다.

for (auto itr = inv.begin(); itr != inv.end();)
{
   // Do some stuff
   if (OK, I decided I need to remove this object from 'inv')
      itr = inv.erase(itr);
   else
      ++itr;
}

0

반복자 수가 불일치하고 일부 반복 후에 유효하지 않은 반복기를 갖게되므로 루프 반복 중에 반복기를 삭제할 수 없습니다.

솔루션 : 1) 원본 벡터의 복사본을 가져옵니다. 2)이 복사본을 사용하여 반복기를 반복합니다. 2) 일부 작업을 수행하고 원본 벡터에서 삭제합니다.

std::vector<IInventory*> inv;
inv.push_back(new Foo());
inv.push_back(new Bar());

std::vector<IInventory*> copyinv = inv;
iteratorCout = 0;
for (IInventory* index : copyinv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
    inv.erase(inv.begin() + iteratorCout);
    iteratorCout++;
}  

0

요소를 하나씩 지우면 쉽게 N ^ 2 성능으로 이어집니다. 지워야 할 요소를 표시하고 루프 후에 한 번에 지우는 것이 좋습니다. 벡터의 유효하지 않은 요소에서 nullptr을 가정 할 수 있다면

std::vector<IInventory*> inv;
// ... push some elements to inv
for (IInventory*& index : inv)
{
    // Do some stuff
    // OK, I decided I need to remove this object from 'inv'...
    {
      delete index;
      index =nullptr;
    }
}
inv.erase( std::remove( begin( inv ), end( inv ), nullptr ), end( inv ) ); 

작동해야합니다.

"Do some stuff"가 벡터의 요소를 변경하지 않고 요소를 제거하거나 유지하는 데만 사용되는 경우 람다로 변환하고 (누군가의 이전 게시물에서 제안한대로) 사용할 수 있습니다.

inv.erase( std::remove_if( begin( inv ), end( inv ), []( Inventory* i )
  {
    // DO some stuff
    return OK, I decided I need to remove this object from 'inv'...
  } ), end( inv ) );
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.