Bạn có thể xóa các thành phần khỏi danh sách std :: trong khi lặp qua nó không?


239

Tôi đã có mã trông như thế này:

for (std::list<item*>::iterator i=items.begin();i!=items.end();i++)
{
    bool isActive = (*i)->update();
    //if (!isActive) 
    //  items.remove(*i); 
    //else
       other_code_involving(*i);
}
items.remove_if(CheckItemNotActive);

Tôi muốn xóa các mục không hoạt động ngay sau khi cập nhật chúng, inorder để tránh đi lại danh sách. Nhưng nếu tôi thêm các dòng nhận xét, tôi sẽ gặp lỗi khi nhận được i++: "Danh sách trình lặp không thể tăng". Tôi đã thử một số lựa chọn thay thế không tăng trong tuyên bố, nhưng tôi không thể làm gì được.

Cách tốt nhất để xóa các mục khi bạn đang đi trong danh sách std :: là gì?


Tôi chưa thấy giải pháp nào dựa trên việc lặp lại. Tôi đã đăng một cái như vậy .
sancho.s RebstateMonicaCellio 13/03/19

Câu trả lời:


286

Bạn phải tăng iterator trước (với i ++) và sau đó loại bỏ phần tử trước đó (ví dụ: bằng cách sử dụng giá trị được trả về từ i ++). Bạn có thể thay đổi mã thành một vòng lặp while như vậy:

std::list<item*>::iterator i = items.begin();
while (i != items.end())
{
    bool isActive = (*i)->update();
    if (!isActive)
    {
        items.erase(i++);  // alternatively, i = items.erase(i);
    }
    else
    {
        other_code_involving(*i);
        ++i;
    }
}

7
Trên thực tế, điều đó không được đảm bảo để làm việc. Với "erase (i ++);", chúng ta chỉ biết rằng giá trị tăng trước được chuyển sang erase () và i được tăng trước dấu chấm phẩy, không nhất thiết phải trước lệnh gọi xóa (). "iterator trước = i ++; erase (trước);" chắc chắn sẽ hoạt động, cũng như sử dụng giá trị trả về
James Curran

58
Không có James, tôi được tăng lên trước khi gọi xóa và giá trị trước đó được truyền cho hàm. Các đối số của hàm phải được đánh giá đầy đủ trước khi hàm được gọi.
Brian Neal

28
@ James Curran: Điều đó không chính xác. TẤT CẢ các đối số được đánh giá đầy đủ trước khi một hàm được gọi.
Martin York

9
Martin York là chính xác. Tất cả các đối số cho một lệnh gọi hàm được đánh giá đầy đủ trước khi một hàm được gọi, không có ngoại lệ. Đó chỉ đơn giản là cách chức năng hoạt động. Và nó không liên quan gì đến ví dụ foo.b (i ++). C (i ++) của bạn (không xác định trong mọi trường hợp)
jalf

75
Việc sử dụng thay thế i = items.erase(i)là an toàn hơn, bởi vì nó tương đương với một danh sách, nhưng vẫn sẽ hoạt động nếu ai đó thay đổi container thành một vectơ. Với một vectơ, erase () di chuyển mọi thứ sang trái để lấp đầy lỗ hổng. Nếu bạn cố gắng loại bỏ mục cuối cùng với mã làm tăng trình lặp sau khi xóa, phần cuối sẽ di chuyển sang bên trái và trình lặp di chuyển sang bên phải - qua phần cuối. Và sau đó bạn sụp đổ.
Eric Seppanen

133

Bạn muốn làm:

i= items.erase(i);

Điều đó sẽ cập nhật chính xác trình lặp để trỏ đến vị trí sau khi trình lặp mà bạn đã xóa.


80
Được cảnh báo rằng bạn không thể bỏ mã đó vào vòng lặp for của mình. Nếu không, bạn sẽ bỏ qua một yếu tố mỗi khi bạn loại bỏ một yếu tố.
Michael Kristofik

2
anh ấy không thể làm tôi--; mỗi lần theo đoạn mã của mình để tránh bỏ qua?
nhiệt tình

1
@enthusiasticgeek, chuyện gì xảy ra nếu i==items.begin()?
MSN

1
@enthusiasticgeek, tại thời điểm đó bạn chỉ nên làm i= items.erase(i);. Đó là hình thức kinh điển và quan tâm đến tất cả những chi tiết đó.
MSN

6
Michael chỉ ra một 'gotcha' khổng lồ, tôi đã phải đối phó với điều tương tự vừa nãy. Phương pháp dễ nhất để tránh nó mà tôi tìm thấy chỉ là phân tách vòng lặp for () thành một lúc () và cẩn thận với việc tăng dần
Anne Quinn

22

Bạn cần thực hiện kết hợp câu trả lời của Kristo và MSN:

// Note: Using the pre-increment operator is preferred for iterators because
//       there can be a performance gain.
//
// Note: As long as you are iterating from beginning to end, without inserting
//       along the way you can safely save end once; otherwise get it at the
//       top of each loop.

std::list< item * >::iterator iter = items.begin();
std::list< item * >::iterator end  = items.end();

while (iter != end)
{
    item * pItem = *iter;

    if (pItem->update() == true)
    {
        other_code_involving(pItem);
        ++iter;
    }
    else
    {
        // BTW, who is deleting pItem, a.k.a. (*iter)?
        iter = items.erase(iter);
    }
}

Tất nhiên, điều khôn ngoan hiệu quả nhất và SuperCool® STL sẽ giống như thế này:

// This implementation of update executes other_code_involving(Item *) if
// this instance needs updating.
//
// This method returns true if this still needs future updates.
//
bool Item::update(void)
{
    if (m_needsUpdates == true)
    {
        m_needsUpdates = other_code_involving(this);
    }

    return (m_needsUpdates);
}

// This call does everything the previous loop did!!! (Including the fact
// that it isn't deleting the items that are erased!)
items.remove_if(std::not1(std::mem_fun(&Item::update)));

Tôi đã xem xét phương pháp SuperCool của bạn, do dự của tôi là lệnh gọi remove_if không làm rõ mục tiêu là xử lý các mục, thay vì xóa chúng khỏi danh sách các mục đang hoạt động. (Các mục không bị xóa vì chúng chỉ không hoạt động, không cần thiết)
AShelly

Tôi cho rằng bạn đúng. Một mặt tôi có xu hướng đề nghị thay đổi tên của 'cập nhật' để loại bỏ sự tối nghĩa, nhưng sự thật là, mã này rất lạ mắt với các functor, nhưng nó cũng không có gì khác ngoài việc không rõ ràng.
Mike

Nhận xét công bằng, hoặc sửa vòng lặp while để sử dụng kết thúc hoặc xóa định nghĩa không sử dụng.
Mike

10

Sử dụng thuật toán std :: remove_if.

Chỉnh sửa: Làm việc với các bộ sưu tập nên như sau: 1. chuẩn bị bộ sưu tập. 2. quy trình thu thập.

Cuộc sống sẽ dễ dàng hơn nếu bạn không trộn các bước này.

  1. std :: remove_if. hoặc list :: remove_if (nếu bạn biết rằng bạn làm việc với danh sách chứ không phải với TCollection)
  2. std :: for_each

2
std :: list có hàm thành viên remove_if hiệu quả hơn thuật toán remove_if (và không yêu cầu thành ngữ "remove-erase").
Brian Neal

5

Dưới đây là một ví dụ sử dụng forvòng lặp lặp lại danh sách và tăng hoặc xác nhận lại trình lặp trong trường hợp một mục bị xóa trong quá trình duyệt qua danh sách.

for(auto i = items.begin(); i != items.end();)
{
    if(bool isActive = (*i)->update())
    {
        other_code_involving(*i);
        ++i;

    }
    else
    {
        i = items.erase(i);

    }

}

items.remove_if(CheckItemNotActive);

4

Thay thế cho phiên bản vòng lặp để trả lời của Kristo.

Bạn mất một số hiệu quả, bạn quay trở lại và sau đó chuyển tiếp một lần nữa khi xóa nhưng để đổi lấy gia tăng vòng lặp bổ sung, bạn có thể có trình lặp được khai báo trong phạm vi vòng lặp và mã trông sạch hơn một chút. Chọn gì phụ thuộc vào ưu tiên của thời điểm này.

Câu trả lời đã hết thời, tôi biết ...

typedef std::list<item*>::iterator item_iterator;

for(item_iterator i = items.begin(); i != items.end(); ++i)
{
    bool isActive = (*i)->update();

    if (!isActive)
    {
        items.erase(i--); 
    }
    else
    {
        other_code_involving(*i);
    }
}

1
Đó cũng là những gì tôi đã sử dụng. Nhưng tôi không chắc nó có đảm bảo hoạt động không nếu phần tử bị loại bỏ là phần tử đầu tiên trong vùng chứa. Đối với tôi, nó hoạt động, tôi nghĩ vậy, nhưng tôi không chắc nó có di động trên các nền tảng hay không.
trololo

Tôi đã không làm "-1", tuy nhiên, danh sách lặp không thể giảm? Ít nhất tôi đã có sự khẳng định từ Visual Studio 2008.
milesma

Miễn là danh sách được liên kết được triển khai dưới dạng danh sách liên kết kép có nút head / stub (được sử dụng như end () rbegin () và khi trống được sử dụng như start () và render () cũng sẽ hoạt động. Tôi không thể nhớ mình đang sử dụng nền tảng nào, nhưng nó cũng hoạt động với tôi, vì việc triển khai có tên ở trên là cách triển khai phổ biến nhất cho danh sách std ::. Nhưng dù sao, gần như chắc chắn rằng điều này đã khai thác một số hành vi không xác định (theo tiêu chuẩn C ++), vì vậy tốt hơn đừng sử dụng nó.
Rafael Gago

lại: iterator cannot be decrementedCác erasephương pháp cần có một random access iterator. Một số triển khai bộ sưu tập cung cấp một forward only iteratornguyên nhân gây ra khẳng định.
Jesse Chisholm

@Jlie Chisholm câu hỏi là về một danh sách std ::, không phải là một thùng chứa tùy ý. std :: list cung cấp các vòng lặp xóa và hai chiều.
Rafael Gago

4

Tôi có sumup nó, đây là ba phương pháp với ví dụ:

1. sử dụng whilevòng lặp

list<int> lst{4, 1, 2, 3, 5};

auto it = lst.begin();
while (it != lst.end()){
    if((*it % 2) == 1){
        it = lst.erase(it);// erase and go to next
    } else{
        ++it;  // go to next
    }
}

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2

2. sử dụng remove_ifchức năng thành viên trong danh sách:

list<int> lst{4, 1, 2, 3, 5};

lst.remove_if([](int a){return a % 2 == 1;});

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2

3. sử dụng std::remove_ifchức năng kết hợp với erasechức năng thành viên:

list<int> lst{4, 1, 2, 3, 5};

lst.erase(std::remove_if(lst.begin(), lst.end(), [](int a){
    return a % 2 == 1;
}), lst.end());

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2

4. sử dụng forvòng lặp, cần lưu ý cập nhật iterator:

list<int> lst{4, 1, 2, 3, 5};

for(auto it = lst.begin(); it != lst.end();++it){
    if ((*it % 2) == 1){
        it = lst.erase(it);  erase and go to next(erase will return the next iterator)
        --it;  // as it will be add again in for, so we go back one step
    }
}

for(auto it:lst)cout<<it<<" ";
cout<<endl;  //4 2 

2

Việc xóa chỉ làm mất hiệu lực các trình vòng lặp trỏ đến các phần tử được loại bỏ.

Vì vậy, trong trường hợp này sau khi xóa * i, i bị vô hiệu và bạn không thể thực hiện gia tăng trên đó.

Những gì bạn có thể làm trước tiên là lưu iterator của phần tử cần loại bỏ, sau đó tăng iterator và sau đó loại bỏ phần tử đã lưu.


2
Sử dụng sau tăng là thanh lịch hơn nhiều.
Brian Neal

2

Nếu bạn nghĩ std::listgiống như một hàng đợi, thì bạn có thể dequeue và enqueue tất cả các mục mà bạn muốn giữ, nhưng chỉ dequeue (và không enqueue) các mục bạn muốn loại bỏ. Đây là một ví dụ mà tôi muốn xóa 5 khỏi danh sách chứa các số 1-10 ...

std::list<int> myList;

int size = myList.size(); // The size needs to be saved to iterate through the whole thing

for (int i = 0; i < size; ++i)
{
    int val = myList.back()
    myList.pop_back() // dequeue
    if (val != 5)
    {
         myList.push_front(val) // enqueue if not 5
    }
}

myList bây giờ sẽ chỉ có các số 1-4 và 6-10.


Cách tiếp cận thú vị nhưng tôi sợ rằng nó có thể chậm mặc dù.
sg7

2

Lặp lại ngược lại để tránh hiệu ứng xóa một phần tử trên các phần tử còn lại sẽ được duyệt qua:

typedef list<item*> list_t;
for ( list_t::iterator it = items.end() ; it != items.begin() ; ) {
    --it;
    bool remove = <determine whether to remove>
    if ( remove ) {
        items.erase( it );
    }
}

PS: xem cái này , ví dụ, liên quan đến việc lặp lại.

PS2: Tôi đã không kiểm tra kỹ lưỡng nếu nó xử lý tốt các phần tử xóa ở phần cuối.


re: avoids the effect of erasing an element on the remaining elementscho một danh sách, có lẽ là có. Đối với một vector có thể không. Đó không phải là một cái gì đó được đảm bảo trên các bộ sưu tập tùy ý. Ví dụ, một bản đồ có thể quyết định tự cân bằng lại.
Jesse Chisholm

1

Bạn có thể viết

std::list<item*>::iterator i = items.begin();
while (i != items.end())
{
    bool isActive = (*i)->update();
    if (!isActive) {
        i = items.erase(i); 
    } else {
        other_code_involving(*i);
        i++;
    }
}

Bạn có thể viết mã tương đương với std::list::remove_if, ít dài dòng hơn và rõ ràng hơn

items.remove_if([] (item*i) {
    bool isActive = (*i)->update();
    if (!isActive) 
        return true;

    other_code_involving(*i);
    return false;
});

Thành std::vector::erase std::remove_ifngữ nên được sử dụng khi các mục là một vectơ thay vì danh sách để giữ sự đồng nhất ở O (n) - hoặc trong trường hợp bạn viết mã chung và các mục có thể là một thùng chứa không có cách nào hiệu quả để xóa các mục đơn lẻ (như vectơ)

items.erase(std::remove_if(begin(items), end(items), [] (item*i) {
    bool isActive = (*i)->update();
    if (!isActive) 
        return true;

    other_code_involving(*i);
    return false;
}));

-4

Tôi nghĩ rằng bạn có một lỗi ở đó, tôi mã theo cách này:

for (std::list<CAudioChannel *>::iterator itAudioChannel = audioChannels.begin();
             itAudioChannel != audioChannels.end(); )
{
    CAudioChannel *audioChannel = *itAudioChannel;
    std::list<CAudioChannel *>::iterator itCurrentAudioChannel = itAudioChannel;
    itAudioChannel++;

    if (audioChannel->destroyMe)
    {
        audioChannels.erase(itCurrentAudioChannel);
        delete audioChannel;
        continue;
    }
    audioChannel->Mix(outBuffer, numSamples);
}

Tôi đoán điều này đã được đánh giá thấp cho sở thích phong cách, vì nó dường như là chức năng. Có, chắc chắn, (1) nó sử dụng một trình vòng lặp bổ sung, (2) gia số vòng lặp ở một vị trí kỳ lạ cho một vòng lặp mà không có lý do chính đáng để đặt nó ở đó, (3) kênh này hoạt động sau khi quyết định xóa thay thế của trước như trong OP. Nhưng nó không phải là một câu trả lời sai.
Jesse Chisholm
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.