คุณสามารถลบองค์ประกอบออกจากรายการ std :: ในขณะที่วนซ้ำมันได้หรือไม่?


239

ฉันมีรหัสที่มีลักษณะดังนี้:

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

ฉันต้องการลบรายการที่ไม่ใช้งานทันทีหลังจากอัปเดตพวกเขาเพื่อหลีกเลี่ยงการเดินรายการอีกครั้ง แต่ถ้าฉันเพิ่มบรรทัดที่ถูกคอมเม้นท์ออกฉันจะได้รับข้อผิดพลาดเมื่อไปถึงi++: "List iterator ไม่สามารถเพิ่มได้" ฉันลองใช้ตัวเลือกอื่นซึ่งไม่ได้เพิ่มในคำสั่ง for แต่ฉันไม่สามารถทำงานอะไรได้

เป็นวิธีที่ดีที่สุดในการลบรายการในขณะที่คุณกำลังเดินรายการ std :: อะไร


ฉันไม่เห็นวิธีแก้ปัญหาใด ๆ ผมโพสต์หนึ่งเช่น
sancho.s ReinstateMonicaCellio

คำตอบ:


286

คุณต้องเพิ่มตัววนซ้ำก่อน (ด้วย i ++) จากนั้นลบองค์ประกอบก่อนหน้า (เช่นโดยใช้ค่าที่ส่งคืนจาก i ++) คุณสามารถเปลี่ยนรหัสเป็นวงขณะที่ดังนี้:

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
จริงๆแล้วมันไม่ได้รับประกันว่าจะทำงาน ด้วย "ลบ (i ++);" เรารู้เพียงว่าค่าที่เพิ่มล่วงหน้าถูกส่งไปยังลบ () และ i เพิ่มค่าก่อนเซมิโคลอนไม่จำเป็นต้องเรียกก่อนที่จะลบ () "iterator prev = i ++; ลบ (ก่อนหน้า);" เพื่อให้แน่ใจว่าการทำงานเป็นใช้ค่าตอบแทน
เจมส์เคอร์แร

58
ไม่ใช่ James ฉันจะถูกเพิ่มค่าก่อนที่จะเรียกการลบและค่าก่อนหน้านี้จะถูกส่งผ่านไปยังฟังก์ชัน ข้อโต้แย้งของฟังก์ชั่นจะต้องได้รับการประเมินอย่างสมบูรณ์ก่อนที่จะเรียกใช้ฟังก์ชั่น
Brian Neal

28
@ James Curran: ไม่ถูกต้อง อาร์กิวเมนต์ทั้งหมดได้รับการประเมินอย่างสมบูรณ์ก่อนที่จะเรียกใช้ฟังก์ชัน
Martin York

9
Martin York ถูกต้อง อาร์กิวเมนต์ทั้งหมดของการเรียกฟังก์ชันถูกประเมินอย่างสมบูรณ์ก่อนที่จะเรียกใช้ฟังก์ชันโดยไม่มีข้อยกเว้น นั่นเป็นเพียงวิธีการทำงานของฟังก์ชั่น และมันก็มีอะไรจะทำอย่างไรกับ foo.b ของคุณ (i ++) ค (i ++) ตัวอย่างเช่น (ซึ่งจะไม่ได้กำหนดในกรณีใด ๆ ).
jalf

75
การใช้ทางเลือกi = items.erase(i)นั้นปลอดภัยกว่าเพราะเทียบเท่ากับรายการ แต่จะยังคงใช้งานได้ถ้ามีคนเปลี่ยนคอนเทนเนอร์เป็นเวกเตอร์ ด้วยเวกเตอร์การลบ () จะย้ายทุกอย่างไปทางซ้ายเพื่อเติมช่องว่าง หากคุณพยายามที่จะลบรายการสุดท้ายด้วยรหัสที่เพิ่มตัววนซ้ำหลังจากลบปลายจะย้ายไปทางซ้ายและตัววนซ้ำเลื่อนไปทางขวา - ผ่านจุดสิ้นสุด และจากนั้นคุณผิดพลาด
Eric Seppanen

133

คุณต้องการจะทำ:

i= items.erase(i);

นั่นจะปรับปรุงตัววนซ้ำให้ถูกต้องเพื่อชี้ไปยังตำแหน่งหลังจากตัววนซ้ำที่คุณลบออกไป


80
ถูกเตือนว่าคุณไม่สามารถวางรหัสนั้นลงใน for-loop ของคุณได้ มิฉะนั้นคุณจะข้ามองค์ประกอบทุกครั้งที่คุณลบออก
Michael Kristofik

2
เขาทำไม่ได้ฉัน -; แต่ละครั้งตามรหัสชิ้นส่วนของเขาเพื่อหลีกเลี่ยงการข้าม?
กระตือรือร้น

1
@enthusiasticgeek จะเกิดอะไรขึ้นถ้าi==items.begin()?
MSN

1
@enthusiasticgeek i= items.erase(i);อย่างนั้นคุณก็ควรจะทำ มันเป็นรูปแบบบัญญัติและดูแลรายละเอียดทั้งหมดเหล่านั้นแล้ว
MSN

6
Michael ชี้ให้เห็นว่า 'gotcha' อันยิ่งใหญ่ฉันต้องจัดการกับสิ่งเดียวกันในตอนนี้ วิธีที่ง่ายที่สุดในการหลีกเลี่ยงสิ่งที่ฉันพบนั้นเป็นเพียงการย่อยสลาย for () วนเป็นเวลา () และระวังการเพิ่มขึ้น
Anne Quinn

22

คุณต้องรวมคำตอบของ Kristo และ 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);
    }
}

แน่นอนสิ่งที่มีประสิทธิภาพมากที่สุดและSuperCool® STL savy จะเป็นดังนี้:

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

ฉันได้พิจารณาวิธี SuperCool ของคุณแล้วความลังเลของฉันคือการเรียกใช้ remove_if ไม่ทำให้ชัดเจนว่าเป้าหมายคือการประมวลผลรายการแทนที่จะลบออกจากรายการสิ่งที่ใช้งานอยู่ (รายการที่ยังไม่ได้ถูกลบเพราะพวกเขาจะกลายเป็นเพียงไม่ได้ใช้งานไม่จำเป็น)
AShelly

ฉันคิดว่าคุณพูดถูก ในมือข้างหนึ่งฉันอยากจะแนะนำให้เปลี่ยนชื่อของ 'update' เพื่อลบความสับสน แต่ความจริงก็คือรหัสนี้เป็นสิ่งที่แปลกใหม่กับฟังก์ชั่น แต่มันก็เป็นอะไรที่ไม่ใช่คลุมเครือ
Mike

ความคิดเห็นที่เป็นธรรมอาจแก้ไขขณะที่ลูปเพื่อใช้สิ้นสุดหรือลบคำจำกัดความที่ไม่ได้ใช้
ไมค์

10

ใช้ std :: remove_if อัลกอริทึม

แก้ไข: ทำงานกับคอลเลกชันควรเป็น: 1. เตรียมคอลเลกชัน 2. กระบวนการรวบรวม

ชีวิตจะง่ายขึ้นถ้าคุณจะไม่ทำตามขั้นตอนนี้

  1. มาตรฐาน :: remove_if หรือ list :: remove_if (ถ้าคุณรู้ว่าคุณทำงานกับ list และไม่ใช่กับ TCollection)
  2. มาตรฐาน :: for_each

2
รายการ std :: มีฟังก์ชั่นสมาชิก remove_if ซึ่งมีประสิทธิภาพมากกว่าอัลกอริทึม remove_if (และไม่ต้องการสำนวน "remove-erase")
Brian Neal

5

นี่คือตัวอย่างการใช้forลูปที่วนซ้ำรายการและเพิ่มหรือลบล้างตัววนซ้ำในกรณีที่ไอเท็มถูกลบระหว่างการสำรวจเส้นทางของรายการ

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

ทางเลือกสำหรับเวอร์ชันลูปถึงคำตอบของ Kristo

คุณสูญเสียประสิทธิภาพบางอย่างคุณย้อนกลับไปข้างหน้าแล้วส่งต่ออีกครั้งเมื่อลบ แต่เพื่อแลกกับการเพิ่มตัววนซ้ำแบบพิเศษคุณสามารถประกาศตัววนซ้ำในขอบเขตลูปและรหัสที่ดูสะอาดขึ้นเล็กน้อย สิ่งที่จะเลือกขึ้นอยู่กับลำดับความสำคัญของช่วงเวลา

คำตอบนั้นหมดเวลาฉันรู้ว่า ...

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
นั่นคือสิ่งที่ฉันใช้เช่นกัน แต่ฉันไม่แน่ใจว่าจะรับประกันได้หรือไม่หากองค์ประกอบที่จะลบออกเป็นองค์ประกอบแรกในคอนเทนเนอร์ สำหรับฉันมันใช้งานได้ฉันคิดว่า แต่ฉันไม่แน่ใจว่ามันพกพาข้ามแพลตฟอร์มได้หรือไม่
trololo

ฉันไม่ได้ทำ "-1" แต่รายการตัววนซ้ำไม่สามารถลดลงได้? อย่างน้อยฉันก็ได้รับการยืนยันจาก Visual Studio 2008
milesma

ตราบใดที่รายการที่เชื่อมโยงถูกนำไปใช้เป็นรายการเชื่อมโยงคู่แบบวงกลมที่มีโหนดหัว / ต้นขั้ว (ใช้เป็น end () rbegin () และเมื่อว่างเปล่าที่ใช้เป็น start () และ rend () ด้วยวิธีนี้จะใช้ได้ ฉันไม่สามารถจำได้ว่าฉันใช้แพลตฟอร์มใด แต่มันก็ใช้ได้กับฉันด้วยเช่นกันเนื่องจากการติดตั้งที่กล่าวถึงข้างต้นเป็นการใช้งานทั่วไปสำหรับรายการ std :: แต่อย่างไรก็ตามมันเกือบจะแน่ใจว่านี่เป็นการใช้ประโยชน์จากพฤติกรรมที่ไม่ได้กำหนด (ตามมาตรฐาน C ++) ดังนั้นจึงไม่ควรใช้
Rafael Gago

เรื่องวิธีการตอบสนองความต้องการ การใช้งานคอลเลกชันบางอย่างให้สิ่งที่ทำให้เกิดการยืนยัน iterator cannot be decrementederaserandom access iteratorforward only iterator
Jesse Chisholm

@Jesse Chisholm คำถามเกี่ยวกับ std :: list ไม่ใช่คอนเทนเนอร์โดยพลการ std :: list ให้ลบและวนซ้ำแบบสองทิศทาง
Rafael Gago

4

ฉันมีข้อสรุปนี่คือสามวิธีด้วยตัวอย่าง:

1. การใช้whileลูป

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. ใช้remove_ifสมาชิก funtion ในรายการ:

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. การใช้std::remove_iffuntion รวมกับeraseฟังก์ชั่นสมาชิก:

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. การใช้forลูปควรทราบการอัพเดตตัววนซ้ำ:

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

การลบจะใช้งานไม่ได้กับตัววนซ้ำที่ชี้ไปยังองค์ประกอบที่ถูกลบเท่านั้น

ดังนั้นในกรณีนี้หลังจากลบ * i ฉันจะใช้การไม่ได้และคุณไม่สามารถเพิ่มได้

สิ่งที่คุณสามารถทำได้คือก่อนบันทึกตัววนซ้ำขององค์ประกอบที่จะถูกลบจากนั้นเพิ่มตัววนซ้ำแล้วลบหนึ่งตัวที่บันทึกไว้


2
การใช้โพสต์ -mentment เป็นสิ่งที่สวยงามมาก
Brian Neal

2

หากคุณคิดว่าstd::listเหมือนคิวคุณสามารถถอนและจัดคิวรายการทั้งหมดที่คุณต้องการเก็บไว้ แต่ถอนเฉพาะรายการ (และไม่เข้าคิว) รายการที่คุณต้องการลบ นี่คือตัวอย่างที่ฉันต้องการลบ 5 จากรายการที่มีตัวเลข 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 ตอนนี้จะมีเพียงตัวเลข 1-4 และ 6-10 เท่านั้น


วิธีการที่น่าสนใจ แต่ฉันกลัวว่ามันอาจจะช้า
sg7

2

การวนซ้ำไปข้างหลังหลีกเลี่ยงผลกระทบของการลบองค์ประกอบบนองค์ประกอบที่เหลือที่จะทำการสำรวจ:

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: ดูนี่เช่นเกี่ยวกับการทำซ้ำย้อนหลัง

PS2: ฉันไม่ได้ทดสอบอย่างถี่ถ้วนถ้ามันจัดการกับองค์ประกอบการลบที่ดีในตอนท้าย


อีก: avoids the effect of erasing an element on the remaining elementsสำหรับรายการอาจจะใช่ สำหรับเวกเตอร์อาจจะไม่ นั่นไม่ใช่สิ่งที่รับประกันได้ในคอลเลกชันเอง ตัวอย่างเช่นแผนที่อาจตัดสินใจปรับสมดุลตัวเอง
Jesse Chisholm

1

คุณสามารถเขียน

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

คุณสามารถเขียนโค้ดที่เทียบเท่าstd::list::remove_ifได้ซึ่งน้อยกว่าและชัดเจนกว่า

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

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

std::vector::erase std::remove_ifสำนวนควรจะใช้เมื่อรายการเป็นเวกเตอร์แทนของรายการเพื่อให้ compexity ที่ O (n) - หรือในกรณีที่คุณเขียนโค้ดทั่วไปและรายการอาจจะเป็นภาชนะที่มีไม่มีวิธีที่มีประสิทธิภาพในการลบรายการเดียว (เช่นเวกเตอร์ก)

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

ฉันคิดว่าคุณมีข้อบกพร่องที่นั่นฉันรหัสด้วยวิธีนี้:

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

ฉันเดาว่าสิ่งนี้ถูกลดระดับลงสำหรับการตั้งค่าสไตล์เนื่องจากดูเหมือนว่าใช้งานได้ ใช่แน่นอน(1)ใช้ตัววนซ้ำพิเศษ(2)การเพิ่มตัววนซ้ำอยู่ในตำแหน่งที่แปลกสำหรับลูปโดยไม่มีเหตุผลที่ดีในการวางไว้ที่นั่น(3)มันทำงานช่องหลังจากการตัดสินใจที่จะลบแทน จากก่อนหน้านี้เหมือนใน OP แต่มันไม่ใช่คำตอบที่ผิด
Jesse Chisholm
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.