Xóa mục khỏi vectơ, trong khi trong vòng lặp 'for' trong phạm vi C ++ 11?


97

Tôi có một vectơ IInventory * và tôi đang lặp qua danh sách bằng cách sử dụng phạm vi C ++ 11 cho, để thực hiện các nội dung với từng phạm vi.

Sau khi thực hiện một số công việc với một cái, tôi có thể muốn xóa nó khỏi danh sách và xóa đối tượng. Tôi biết tôi có thể gọi deletecon trỏ bất cứ lúc nào để dọn dẹp nó, nhưng cách thích hợp để xóa nó khỏi vectơ, khi ở trong forvòng lặp phạm vi là gì? Và nếu tôi xóa nó khỏi danh sách thì vòng lặp của tôi có bị vô hiệu không?

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
Nếu bạn muốn trở nên lạ mắt, bạn có thể sử dụng std::remove_ifvới một vị từ "does things" và sau đó trả về true nếu bạn muốn xóa phần tử.
Benjamin Lindley

Có lý do gì khiến bạn không thể chỉ thêm một bộ đếm chỉ mục và sau đó sử dụng một cái gì đó như inv.erase (chỉ mục)?
TomJ

4
@TomJ: Điều đó vẫn sẽ làm hỏng việc lặp lại.
Ben Voigt

@TomJ và nó sẽ là một kẻ hủy diệt hiệu suất - trên mỗi lần xóa, bạn có thể di chuyển tất cả các phần tử sau phần bị xóa.
Ivan,

1
@BenVoigt Tôi đề nghị chuyển sang std::listdưới đây
bobobobo

Câu trả lời:


94

Không, bạn không thể. Dựa trên phạm vi forlà khi bạn cần truy cập từng phần tử của vùng chứa một lần.

Bạn nên sử dụng forvòng lặp bình thường hoặc một trong các vòng lặp anh em của nó nếu bạn cần sửa đổi vùng chứa khi bạn thực hiện, truy cập một phần tử nhiều lần hoặc lặp lại theo kiểu phi tuyến tính qua vùng chứa.

Ví dụ:

auto i = std::begin(inv);

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

5
Thành ngữ xóa xóa sẽ không áp dụng ở đây?
Naveen

4
@Naveen Tôi quyết định không cố gắng làm điều đó vì rõ ràng anh ấy cần phải lặp lại mọi mục, tính toán với nó và sau đó có thể xóa nó khỏi vùng chứa. Erase-remove nói rằng bạn chỉ xóa các phần tử mà một vị từ trả về true, AFAIU, và có vẻ tốt hơn theo cách này để không trộn logic lặp vào với vị từ.
Seth Carnegie

4
@SethCarnegie Erase-remove với một lambda cho vị thanh lịch cho phép đó (vì đây đã C ++ là 11)
Potatoswatter

11
Không thích giải pháp này, đó là O (N ^ 2) cho hầu hết các thùng chứa. remove_iftốt hơn.
Ben Voigt

6
câu trả lời này đúng, erasetrả về một trình lặp hợp lệ mới. nó có thể không hiệu quả, nhưng nó đảm bảo hoạt động.
sp2danny

56

Mỗi khi một phần tử bị xóa khỏi vectơ, bạn phải giả định rằng các trình vòng lặp tại hoặc sau phần tử bị xóa không còn hợp lệ nữa, bởi vì mỗi phần tử tiếp theo phần tử bị xóa sẽ được di chuyển.

Vòng lặp for dựa trên phạm vi chỉ là đường cú pháp cho vòng lặp "bình thường" bằng cách sử dụng trình vòng lặp, vì vậy điều trên áp dụng.

Điều đó đang được nói, bạn có thể chỉ cần:

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

5
" bởi vì có khả năng vectơ đã phân bổ lại khối bộ nhớ trong đó nó giữ các phần tử của nó " Không, a vectorsẽ không bao giờ tái phân bổ do lệnh gọi tới erase. Lý do các trình vòng lặp bị vô hiệu là vì mỗi phần tử tiếp theo phần tử bị xóa sẽ bị di chuyển.
ildjarn

2
Mặc định chụp [&]là thích hợp, để cho phép anh ta "làm một số việc" với các biến cục bộ.
Potatoswatter

2
Đây không giống bất kỳ đơn giản hơn một vòng lặp lặp dựa trên, ngoài ra bạn cần phải nhớ để bao quanh mình remove_ifvới .erase, nếu không có gì xảy ra.
bobobobo

2
@bobobobo Nếu theo "vòng lặp dựa trên trình lặp", bạn có nghĩa là câu trả lời của Seth Carnegie , đó là trung bình O (n ^ 2). std::remove_iflà O (n).
Branko Dimitrijevic

Bạn có một điểm thực sự tốt, bằng cách hoán đổi các phần tử vào phía sau của danh sách và tránh thực sự di chuyển các phần tử cho đến khi hoàn thành tất cả các yêu tinh "cần loại bỏ" (điều này remove_ifphải làm trong nội bộ). Tuy nhiên nếu bạn có một vectortrong 5 yếu tố và chỉ có bạn .erase() 1 tại một thời điểm, không có tác động hiệu quả cho việc sử dụng lặp vs remove_if. Nếu danh sách lớn hơn, bạn thực sự nên chuyển sang std::listnơi có nhiều loại bỏ giữa danh sách.
bobobobo

16

Tốt nhất bạn không nên sửa đổi vectơ trong khi lặp lại nó. Sử dụng thành ngữ xóa - xóa. Nếu bạn làm vậy, bạn có thể gặp phải một số vấn đề. Vì trong một vectormột erasemất hiệu lực tất cả các vòng lặp bắt đầu với phần tử bị xoá hoàn toàn tối đa các end()bạn sẽ cần phải chắc chắn rằng vòng lặp của bạn vẫn có hiệu lực bằng cách sử dụng:

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

Lưu ý rằng bạn cần b != v.end()kiểm tra nguyên trạng. Nếu bạn cố gắng tối ưu hóa nó như sau:

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

bạn sẽ chạy vào UB vì của bạn ebị vô hiệu sau lần erasegọi đầu tiên .


@ildjarn: Đúng vậy! Đó là một lỗi đánh máy.
dirkgently

2
Đây không phải là thành ngữ xóa bỏ. Không có cuộc gọi đến std::removevà đó là O (N ^ 2) không phải O (N).
Potatoswatter

1
@Potatoswatter: Tất nhiên là không. Tôi đang cố gắng chỉ ra các vấn đề với việc xóa khi bạn lặp lại. Tôi đoán từ ngữ của tôi đã không vượt qua tập hợp?
dirkgently

5

Có phải là một yêu cầu nghiêm ngặt để loại bỏ các phần tử trong vòng lặp đó? Nếu không, bạn có thể đặt các con trỏ bạn muốn xóa thành NULL và thực hiện một lần chuyển qua vector để loại bỏ tất cả các con trỏ 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

xin lỗi vì đã đăng ký và cũng xin lỗi nếu kiến ​​thức chuyên môn về c ++ của tôi cản trở câu trả lời của tôi, nhưng nếu bạn cố gắng lặp lại từng mục và thực hiện các thay đổi có thể có (như xóa chỉ mục), hãy thử sử dụng từ khóa sau cho vòng lặp.

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

//do stuff
//erase index x

}

khi xóa chỉ mục x, vòng lặp tiếp theo sẽ dành cho mục "phía trước" lần lặp cuối cùng. tôi thực sự hy vọng điều này sẽ giúp ai đó


chỉ cần đừng quên khi sử dụng x để truy cập một chỉ mục nhất định, hãy làm x-1 lol
lilbigwill99

1

OK, tôi đến trễ, nhưng dù sao: Xin lỗi, không đúng những gì tôi đọc cho đến nay - đó có thể, bạn chỉ cần hai vòng lặp:

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

Chỉ cần sửa đổi giá trị mà một trình vòng lặp trỏ tới không làm mất hiệu lực của bất kỳ trình vòng lặp nào khác, vì vậy chúng ta có thể làm điều này mà không cần phải lo lắng. Trên thực tế, std::remove_if(ít nhất là triển khai gcc) thực hiện một cái gì đó rất tương tự (sử dụng vòng lặp cổ điển ...), chỉ không xóa bất kỳ thứ gì và không xóa.

Tuy nhiên, hãy lưu ý rằng đây không phải là chuỗi an toàn (!) - tuy nhiên, điều này cũng áp dụng cho một số giải pháp khác ở trên ...


Cái quái gì vậy. Đây là một sự quá mức cần thiết.
Kesse

@Kesse Thật không? Đây là thuật toán hiệu quả nhất mà bạn có thể nhận được với vectơ (không quan trọng là vòng lặp dựa trên phạm vi hay vòng lặp trình lặp cổ điển): Bằng cách này, bạn di chuyển mỗi phần tử trong vectơ nhiều nhất một lần và bạn lặp lại toàn bộ vectơ chính xác một lần. Bạn sẽ di chuyển bao nhiêu lần các phần tử tiếp theo và lặp lại trên vectơ nếu bạn đã xóa từng phần tử phù hợp qua erase(tất nhiên là với điều kiện bạn xóa nhiều hơn một phần tử duy nhất)?
Aconcagua

1

Tôi sẽ hiển thị với ví dụ, ví dụ dưới đây loại bỏ các phần tử lẻ khỏi vector:

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;

}

đầu ra aw dưới đây:

024
024
024

Hãy nhớ rằng, phương thức erasesẽ trả về trình vòng lặp tiếp theo của trình vòng lặp đã truyền.

Từ đây , chúng ta có thể sử dụng thêm một phương thức tạo:

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

Xem tại đây để biết cách sử dụng std::remove_if. https://en.cppreference.com/w/cpp/algorithm/remove


1

Đối lập với tiêu đề chủ đề này, tôi sẽ sử dụng hai đường chuyền:

#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

Một giải pháp thanh lịch hơn nhiều sẽ là chuyển sang std::list(giả sử bạn không cần truy cập ngẫu nhiên nhanh).

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

Sau đó, bạn có thể xóa bằng .remove_ifvà một hàm C ++ trong một dòng:

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

Vì vậy, ở đây tôi chỉ viết một functor chấp nhận một đối số (các Widget*). Giá trị trả về là điều kiện để xóa a Widget*khỏi danh sách.

Tôi thấy cú pháp này dễ hiểu. Tôi không nghĩ rằng tôi sẽ sử dụng remove_ifcho std :: vectors - có quá nhiều inv.begin()inv.end()nhiễu ở đó, bạn có lẽ tốt hơn nên sử dụng xóa dựa trên chỉ mục số nguyên hoặc chỉ xóa dựa trên trình lặp thông thường cũ (như hình minh họa phía dưới). Nhưng std::vectordù sao thì bạn cũng không thực sự nên xóa từ listgiữa danh sách , vì vậy bạn nên chuyển sang sử dụng một đối với trường hợp thường xuyên xóa giữa danh sách này.

Tuy nhiên, lưu ý rằng tôi đã không có cơ hội để gọi deletenhững Widget*thứ đã bị xóa. Để làm điều đó, nó sẽ giống như sau:

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

Bạn cũng có thể sử dụng một vòng lặp dựa trên trình lặp thông thường như sau:

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

Nếu bạn không thích độ dài của for( list<Widget*>::iterator iter = widgets.begin() ; ..., bạn có thể sử dụng

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

1
Tôi không nghĩ rằng bạn hiểu cách thức hoạt động của remove_ifmột std::vectortác phẩm thực sự và cách nó giữ độ phức tạp ở mức O (N).
Ben Voigt,

Điều đó không quan trọng. Xóa từ giữa a std::vectorsẽ luôn trượt từng phần tử sau phần tử mà bạn đã xóa, đưa ra std::listlựa chọn tốt hơn nhiều.
bobobobo

1
Không, nó sẽ không "trượt từng phần tử lên một". remove_ifsẽ trượt từng phần tử lên theo số khoảng trống được giải phóng. Bởi thời gian bạn chiếm dụng bộ nhớ cache, remove_iftrên một std::vectornhanh hơn so với việc loại bỏ khả năng từ một std::list. Và bảo tồn O(1)quyền truy cập ngẫu nhiên.
Ben Voigt

1
Sau đó, bạn có một câu trả lời tuyệt vời khi tìm kiếm một câu hỏi. Câu hỏi này nói về việc lặp lại danh sách, là O (N) cho cả hai vùng chứa. Và loại bỏ các phần tử O (N), cũng là O (N) cho cả hai vùng chứa.
Ben Voigt

2
Đánh dấu trước là không cần thiết; hoàn toàn có thể làm được điều này trong một lần vượt qua. Bạn chỉ cần theo dõi "phần tử tiếp theo được kiểm tra" và "vị trí tiếp theo sẽ được lấp đầy". Hãy coi nó giống như việc xây dựng một bản sao của danh sách, được lọc dựa trên vị từ. Nếu vị từ trả về true, hãy bỏ qua phần tử, nếu không hãy sao chép nó. Nhưng bản sao danh sách được tạo tại chỗ và hoán đổi / di chuyển được sử dụng thay vì sao chép.
Ben Voigt

0

Tôi nghĩ tôi sẽ làm như sau ...

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

bạn không thể xóa trình vòng lặp trong quá trình lặp lại vòng lặp vì số lượng trình vòng lặp không khớp và sau một số lần lặp bạn sẽ có trình vòng lặp không hợp lệ.

Giải pháp: 1) lấy bản sao của vectơ gốc 2) lặp lại trình lặp bằng cách sử dụng bản sao này 2) thực hiện một số công việc và xóa nó khỏi vectơ gốc.

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

Việc xóa từng phần tử một dễ dẫn đến hiệu suất N ^ 2. Tốt hơn là đánh dấu các phần tử cần được xóa và xóa chúng ngay sau vòng lặp. Nếu tôi có thể cho rằng nullptr trong phần tử không hợp lệ trong vectơ của bạn, thì

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

nên làm việc.

Trong trường hợp "Thực hiện một số công việc" của bạn không thay đổi các phần tử của vectơ và chỉ được sử dụng để đưa ra quyết định xóa hoặc giữ phần tử, bạn có thể chuyển đổi nó thành lambda (như đã được đề xuất trong bài đăng trước đó của ai đó) và sử dụng

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 ) );
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.