การลบองค์ประกอบออกจาก 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 หรือการใช้งานนี้เฉพาะ ฉันใช้ gcc 4.3.3 บน Ubuntu 10.04 (รุ่น 32 บิต) โดยวิธีการ

ขอบคุณ!

โซลูชันที่เสนอ:

นี่เป็นวิธีที่ถูกต้องในการทำซ้ำและลบองค์ประกอบออกจากชุดหรือไม่

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

หากมีเงื่อนไขการทดสอบหลายอย่างในขณะนั้นแต่ละเงื่อนไขจะต้องเพิ่มตัววนซ้ำ ฉันชอบรหัสนี้ดีกว่าเนื่องจากตัววนซ้ำเพิ่มขึ้นในที่เดียวเท่านั้นทำให้รหัสผิดพลาดน้อยลงและอ่านง่ายขึ้น


1
ถามและตอบ: stackoverflow.com/questions/263945/…
Martin York

3
ที่จริงแล้วฉันอ่านคำถามนี้ (และอื่น ๆ ) ก่อนถามฉัน แต่เนื่องจากพวกเขาเกี่ยวข้องกับตู้คอนเทนเนอร์ STL อื่นและเนื่องจากเห็นได้ชัดว่าการทดสอบครั้งแรกของฉันทำงานได้ฉันคิดว่ามีความแตกต่างระหว่างพวกเขา หลังจากคำตอบของ Matt ฉันคิดว่าจะใช้ valgrind แม้ว่าฉันจะชอบโซลูชันใหม่ของฉันมากกว่าโซลูชันอื่นเนื่องจากจะลดโอกาสของข้อผิดพลาดโดยการเพิ่มตัววนซ้ำในที่เดียวเท่านั้น ขอบคุณสำหรับความช่วยเหลือ!
pedromanoel

1
@ pedromanoel ++itควรมีประสิทธิภาพมากกว่าit++เพราะไม่จำเป็นต้องใช้ตัวคัดลอกชั่วคราวที่มองไม่เห็นของ 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;
    }
}

โปรดทราบว่ามัน ++ เป็น postfix ดังนั้นจึงส่งผ่านตำแหน่งเก่าเพื่อลบ แต่ก่อนจะข้ามไปยังตำแหน่งที่ใหม่กว่าเนื่องจากตัวดำเนินการ

การอัปเดต 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 ใช้ตัววนซ้ำ (const_iterator ใน C ++ 11) ทันที
tartaruga_casco_mole

19

หากคุณรันโปรแกรมผ่าน valgrind คุณจะเห็นข้อผิดพลาดในการอ่านมากมาย กล่าวอีกนัยหนึ่งใช่ตัวทำซ้ำกำลังถูกทำให้เป็นโมฆะ แต่คุณโชคดีในตัวอย่างของคุณ (หรือโชคไม่ดีจริงๆเนื่องจากคุณไม่เห็นผลกระทบด้านลบจากพฤติกรรมที่ไม่ได้กำหนด) ทางออกหนึ่งในการทำเช่นนี้คือการสร้างตัววนซ้ำชั่วคราวเพิ่ม temp ลบตัววนซ้ำเป้าหมายจากนั้นตั้งค่าเป้าหมายเป็น temp ตัวอย่างเช่นเขียนลูปของคุณใหม่ดังนี้:

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. - ผลลัพธ์เฉพาะที่นี่อาจขึ้นกับรายละเอียดการใช้งานของวิธีการลบและตั้งตัววนซ้ำมากกว่าในระยะของดวงจันทร์ (ไม่ใช่อันนั้น ควรพึ่งพารายละเอียดการใช้งาน) ;)
ลุงเบ็น

1
STL เพิ่มความหมายใหม่มากมายให้กับ "พฤติกรรมที่ไม่ได้กำหนด" ตัวอย่างเช่น "Microsoft คิดว่าสมาร์ทเพื่อปรับปรุงสเปคโดยอนุญาตให้std::set::eraseส่งคืนตัววนซ้ำดังนั้นโค้ด MSVC ของคุณจะถูกโจมตีเมื่อคอมไพล์โดย gcc" หรือ "Microsoft ทำการตรวจสอบstd::bitset::operator[]อย่างถูกต้อง รวบรวมข้อมูลเมื่อรวบรวมกับ MSVC " STL ไม่มีการดำเนินการที่ไม่ซ้ำกันและข้อมูลจำเพาะของมันคือการเติบโตชี้แจงระเบียบป่องจึงไม่น่าแปลกใจที่การลบองค์ประกอบจากภายในห่วงต้องใช้ความเชี่ยวชาญโปรแกรมเมอร์อาวุโส ...
Kuroi Neko

2

เพียงเพื่อเตือนว่าในกรณีของ deque container โซลูชันทั้งหมดที่ตรวจสอบความเท่าเทียมกันของ deque iterator เป็น numbers.end () อาจจะล้มเหลวใน gcc 4.8.4 กล่าวคือการลบองค์ประกอบของ deque โดยทั่วไปจะทำให้ตัวชี้ทำให้ตัวเลขเป็นโมฆะ ():

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

โปรดทราบว่าในขณะที่การแปลงการแปลถูกต้องในกรณีนี้ตัวชี้สิ้นสุดได้รับการทำให้ใช้งานไม่ได้ระหว่างทาง ด้วยขนาดที่แตกต่างทำให้ข้อผิดพลาดปรากฏชัดเจนมากขึ้น:

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 จะมี "การลบคอนเทนเนอร์แบบสม่ำเสมอ" และคุณจะสามารถเขียน:

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

และที่จะทำงานให้vector, set, dequeฯลฯ ดูcppReferenceสำหรับข้อมูลเพิ่มเติม


1

พฤติกรรมนี้เป็นการใช้งานเฉพาะ เพื่อรับประกันความถูกต้องของตัววนซ้ำคุณควรใช้ "it = numbers.erase (it);" คำสั่งถ้าคุณต้องการที่จะลบองค์ประกอบและเพียงแค่วนซ้ำ iterator ในกรณีอื่น


1
Set<T>::eraseเวอร์ชันจะไม่ส่งคืนตัววนซ้ำ
Arkaitz Jimenez

4
จริง ๆ แล้วมันทำ แต่เฉพาะในการใช้งาน MSVC ดังนั้นนี่คือคำตอบเฉพาะของการนำไปใช้อย่างแท้จริง :)
ยูจีน

1
@Eugene มันใช้งานได้กับ C ++ 11
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ฟังก์ชัน lambda จำเป็นต้องได้รับประเภทนั้น (หรือการอ้างอิงถึงประเภทนั้น) ที่ส่งผ่านเป็นอาร์กิวเมนต์

 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 ส่งคืนค่าจริงและเลื่อนเนื้อหานั้นไปยังจุดเริ่มต้นของคอนเทนเนอร์ ' it' ชี้ไปยังวัตถุที่ไม่ได้กำหนดซึ่งถือเป็นขยะได้ วัตถุจาก 'it' ถึง m_bullets.end () สามารถลบได้เนื่องจากมีหน่วยความจำ แต่มีขยะดังนั้นเมธอด 'ลบ' จึงถูกเรียกบนช่วงนั้น


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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.