Xóa các phần tử khỏi std :: set trong khi lặp


147

Tôi cần phải trải qua một tập hợp và loại bỏ các yếu tố đáp ứng tiêu chí được xác định trước.

Đây là mã kiểm tra tôi đã viết:

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

Lúc đầu, tôi nghĩ rằng việc xóa một phần tử khỏi tập hợp trong khi lặp qua nó sẽ làm mất hiệu lực của trình vòng lặp và gia số tại vòng lặp for sẽ có hành vi không xác định. Mặc dù, tôi đã thực thi mã kiểm tra này và tất cả đều diễn ra tốt đẹp, và tôi không thể giải thích tại sao.

Câu hỏi của tôi: Đây có phải là hành vi được xác định cho các bộ std hoặc việc triển khai này là cụ thể? Tôi đang sử dụng gcc 4.3.3 trên Ubuntu 10.04 (phiên bản 32 bit).

Cảm ơn!

Giải pháp đề xuất:

Đây có phải là một cách chính xác để lặp và xóa các phần tử khỏi tập hợp?

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

Chỉnh sửa: GIẢI PHÁP ƯU ĐÃI

Tôi đã tìm ra một giải pháp có vẻ thanh lịch hơn đối với tôi, mặc dù nó hoàn toàn giống nhau.

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

Nếu có một vài điều kiện kiểm tra bên trong, mỗi một trong số chúng phải tăng vòng lặp. Tôi thích mã này tốt hơn bởi vì trình vòng lặp chỉ được tăng ở một nơi , làm cho mã ít bị lỗi hơn và dễ đọc hơn.



3
Trên thực tế, tôi đã đọc câu hỏi này (và những câu hỏi khác) trước khi hỏi tôi, nhưng vì chúng có liên quan đến các thùng chứa STL khác và vì thử nghiệm ban đầu của tôi rõ ràng có hiệu quả, tôi nghĩ rằng có một số khác biệt giữa chúng. Chỉ sau câu trả lời của Matt, tôi mới nghĩ đến việc sử dụng valgrind. Mặc dù vậy, tôi thích giải pháp MỚI của mình hơn các giải pháp khác vì nó giảm khả năng xảy ra lỗi bằng cách tăng trình lặp ở một nơi duy nhất. Cảm ơn tất cả các bạn đã giúp đỡ!
pedromanoel

1
@pedromanoel ++itsẽ hiệu quả hơn một chút so với it++vì nó không yêu cầu sử dụng bản sao tạm thời vô hình của trình vòng lặp. Phiên bản của Kornel trong khi dài hơn đảm bảo rằng các phần tử không được lọc được lặp lại một cách hiệu quả nhất.
Alnitak

@Alnitak Tôi chưa nghĩ đến điều đó, nhưng tôi nghĩ rằng sự khác biệt trong hiệu suất sẽ không quá lớn. Bản sao được tạo trong phiên bản của anh ấy, nhưng chỉ cho các yếu tố phù hợp. Vì vậy, mức độ tối ưu hóa hoàn toàn phụ thuộc vào cấu trúc của tập hợp. Trong một thời gian, tôi đã tối ưu hóa mã trước, làm tổn thương khả năng đọc và tốc độ mã hóa trong quá trình ... Vì vậy, tôi sẽ thực hiện một số thử nghiệm trước khi sử dụng cách khác.
pedromanoel

Câu trả lời:


178

Đây là phụ thuộc thực hiện:

Tiêu chuẩn 23.1.2.8:

Các thành viên chèn sẽ không ảnh hưởng đến tính hợp lệ của các trình vòng lặp và các tham chiếu đến vùng chứa và các thành viên xóa sẽ chỉ vô hiệu hóa các trình vòng lặp và các tham chiếu đến các phần tử bị xóa.

Có lẽ bạn có thể thử điều này - đây là tiêu chuẩn tuân thủ:

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        numbers.erase(it++);
    }
    else {
        ++it;
    }
}

Lưu ý rằng nó ++ là hậu tố, do đó nó chuyển vị trí cũ để xóa, nhưng trước tiên nhảy sang vị trí mới hơn do toán tử.

Cập nhật 2015.10.27: C ++ 11 đã giải quyết được lỗi. iterator erase (const_iterator position);trả về một iterator cho phần tử theo sau phần tử cuối cùng bị loại bỏ (hoặc set::end, nếu phần tử cuối cùng bị loại bỏ). Vậy phong cách C ++ 11 là:

for (auto it = numbers.begin(); it != numbers.end(); ) {
    if (*it % 2 == 0) {
        it = numbers.erase(it);
    }
    else {
        ++it;
    }
}

2
Điều này không hoạt động với deque trên MSVC2013. Việc triển khai của họ là có lỗi hoặc có một yêu cầu khác ngăn cản việc này hoạt động deque. Thông số STL rất phức tạp đến nỗi bạn không thể mong đợi tất cả các triển khai tuân theo nó, hãy để một mình lập trình viên bình thường của bạn ghi nhớ nó. STL là một con quái vật ngoài việc thuần hóa, và vì không có triển khai duy nhất (và các bộ thử nghiệm, nếu có, rõ ràng không bao gồm các trường hợp rõ ràng như xóa các yếu tố trong một vòng lặp), điều đó làm cho STL trở thành một đồ chơi dễ vỡ có thể đi lên một tiếng nổ khi bạn nhìn nó sang một bên.
Kuroi neko

@MatthieuM. Nó làm trong C ++ 11. Trong C ++ 17, phải mất iterator (const_iterator trong C ++ 11).
tartaruga_casco_mole

18

Nếu bạn chạy chương trình của mình thông qua valgrind, bạn sẽ thấy một loạt lỗi đọc. Nói cách khác, vâng, các trình vòng lặp đang bị vô hiệu, nhưng bạn đang gặp may mắn trong ví dụ của mình (hoặc thực sự không may mắn, vì bạn không thấy tác động tiêu cực của hành vi không xác định). Một giải pháp cho vấn đề này là tạo một trình vòng lặp tạm thời, tăng temp, xóa trình lặp đích, sau đó đặt mục tiêu thành temp. Ví dụ, viết lại vòng lặp của bạn như sau:

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

Nếu đó chỉ là điều kiện quan trọng & không yêu cầu khởi tạo trong phạm vi hoặc hậu vận hành, thì tốt hơn nên sử dụng whilevòng lặp. tức for ( ; it != numbers.end(); )là hiển thị rõ hơn vớiwhile (it != numbers.end())
iammilind

7

Bạn hiểu sai "hành vi không xác định" nghĩa là gì. Hành vi không xác định không có nghĩa là "nếu bạn làm điều này, chương trình của bạn sẽ bị sập hoặc tạo ra kết quả không mong muốn." Nó có nghĩa là "nếu bạn làm điều này, chương trình của bạn có thể bị sập hoặc tạo ra kết quả không mong muốn" hoặc làm bất cứ điều gì khác, tùy thuộc vào trình biên dịch, hệ điều hành của bạn, giai đoạn của mặt trăng, v.v.

Nếu một cái gì đó thực thi mà không sụp đổ và hành xử như bạn mong đợi, đó không phải là bằng chứng cho thấy đó không phải là hành vi không xác định. Tất cả những gì nó chứng minh là hành vi của nó đã xảy ra như được quan sát cho lần chạy cụ thể đó sau khi biên dịch với trình biên dịch cụ thể đó trên hệ điều hành cụ thể đó.

Xóa một phần tử khỏi một tập hợp làm mất hiệu lực của trình vòng lặp thành phần tử bị xóa. Sử dụng một trình vòng lặp không hợp lệ là hành vi không xác định. Nó chỉ xảy ra rằng hành vi được quan sát là những gì bạn dự định trong trường hợp cụ thể này; Nó không có nghĩa là mã là chính xác.


Ồ, tôi nhận thức rõ rằng hành vi không xác định cũng có thể có nghĩa là "Nó hoạt động với tôi, nhưng không phải cho tất cả mọi người". Đó là lý do tại sao tôi hỏi câu hỏi này, vì tôi không biết hành vi này có đúng hay không. Nếu nó là, hơn tôi sẽ chỉ để lại như vậy. Sử dụng một vòng lặp while sẽ giải quyết vấn đề của tôi, sau đó? Tôi chỉnh sửa câu hỏi của tôi với giải pháp đề xuất của tôi. Làm ơn hãy kiểm tra nó.
pedromanoel

Nó cũng có tác dụng với tôi. Nhưng khi tôi thay đổi điều kiện thành if (n > 2 && n < 7 )thì tôi nhận được 0 1 2 4 7 8 9. - Kết quả cụ thể ở đây có lẽ phụ thuộc nhiều hơn vào chi tiết thực hiện của phương pháp xóa và đặt các vòng lặp, thay vì vào pha của mặt trăng (không phải là một nên bao giờ dựa vào chi tiết thực hiện). ;)
ChúBens

1
STL bổ sung rất nhiều ý nghĩa mới cho "hành vi không xác định". Ví dụ: "Microsoft nghĩ thông minh để tăng cường thông số kỹ thuật bằng cách cho phép std::set::erasetrả về một trình vòng lặp, do đó mã MSVC của bạn sẽ bị lỗi khi được biên dịch bởi gcc" hoặc "Microsoft thực hiện kiểm tra ràng buộc std::bitset::operator[]để thuật toán bitet được tối ưu hóa cẩn thận của bạn sẽ chậm lại thu thập dữ liệu khi được biên dịch bằng MSVC ". STL không có triển khai duy nhất và thông số kỹ thuật của nó là một mớ hỗn độn đang tăng theo cấp số nhân, do đó, không có gì lạ khi xóa các yếu tố từ bên trong một vòng lặp đòi hỏi phải có chuyên môn lập trình viên cao cấp ...
Kuroi neko

2

Chỉ cần cảnh báo, trong trường hợp thùng chứa deque, tất cả các giải pháp kiểm tra đẳng thức lặp deque với số.end () có thể sẽ thất bại trên gcc 4.8.4. Cụ thể, việc xóa một phần tử của deque thường làm mất hiệu lực con trỏ tới số.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;
    }
  }
}

Đầu ra:

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

Lưu ý rằng trong khi chuyển đổi deque là chính xác trong trường hợp cụ thể này, con trỏ cuối đã bị vô hiệu hóa trên đường đi. Với deque có kích thước khác nhau, lỗi rõ ràng hơn:

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

Đầu ra:

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)

Đây là một trong những cách để khắc phục điều này:

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

Chìa khóa đang tồn tại do not trust an old remembered dq.end() value, always compare to a new call to dq.end().
Jesse Chisholm

2

C ++ 20 sẽ có "xóa bộ chứa đồng nhất" và bạn sẽ có thể viết:

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

Và điều đó sẽ làm việc cho vector, set, dequevv Xem cppReference để biết thêm.


1

Hành vi này là thực hiện cụ thể. Để đảm bảo tính chính xác của iterator, bạn nên sử dụng "it = Numbers.erase (it);" câu lệnh nếu bạn cần xóa phần tử và chỉ đơn giản là lặp đi lặp lại trong trường hợp khác.


1
Set<T>::erasephiên bản không trả về iterator.
Arkaitz Jimenez

4
Trên thực tế, nhưng chỉ thực hiện MSVC. Vì vậy, đây thực sự là một câu trả lời cụ thể thực hiện. :)
Eugene

1
@Eugene Nó thực hiện điều đó cho tất cả các triển khai với C ++ 11
mastov

Một số thực hiện gcc 4.8với c++1ycó một lỗi trong xóa. it = collection.erase(it);được cho là hoạt động, nhưng có thể an toàn hơn khi sử dụngcollection.erase(it++);
Jesse Chisholm

1

Tôi nghĩ rằng việc sử dụng phương thức STL ' remove_if' có thể giúp ngăn ngừa một số vấn đề kỳ lạ khi cố gắng xóa đối tượng được bao bọc bởi trình vòng lặp.

Giải pháp này có thể kém hiệu quả.

Giả sử chúng ta có một số loại container, như vector hoặc danh sách gọi là m_bullets:

Bullet::Ptr is a shared_pr<Bullet>

' it' là trình lặp mà ' remove_if' trả về, đối số thứ ba là hàm lambda được thực thi trên mọi phần tử của vùng chứa. Bởi vì bộ chứa chứa Bullet::Ptr, hàm lambda cần có loại đó (hoặc tham chiếu đến loại đó) được truyền dưới dạng đối số.

 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' loại bỏ vùng chứa trong đó hàm lambda trả về giá trị true và chuyển nội dung đó sang phần đầu của vùng chứa. ' it' Chỉ đến một đối tượng không xác định có thể được coi là rác. Các đối tượng từ 'it' đến m_bullets.end () có thể bị xóa, vì chúng chiếm bộ nhớ, nhưng chứa rác, do đó phương thức 'xóa' được gọi trên phạm vi đó.


0

Tôi đã gặp một vấn đề cũ và thấy mã bên dưới dễ hiểu hơn theo cách giải quyết trên.

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

Điều này chỉ hoạt động nếu bạn sẽ luôn luôn xóa mọi mục. OP là về việc chọn lọc xóa các mục và vẫn có các trình vòng lặp hợp lệ.
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.