การลบรายการออกจากเวกเตอร์ในขณะที่อยู่ใน C ++ 11 ช่วง 'for' loop?


100

ฉันมีเวกเตอร์ของ IInventory * และฉันกำลังวนรอบรายการโดยใช้ช่วง C ++ 11 เพื่อทำสิ่งต่างๆกับแต่ละอัน

หลังจากดำเนินการบางอย่างแล้วฉันอาจต้องการลบออกจากรายการและลบวัตถุนั้น ฉันรู้ว่าฉันสามารถเรียกdeleteตัวชี้ได้ทุกเมื่อเพื่อทำความสะอาด แต่วิธีใดที่เหมาะสมในการลบออกจากเวกเตอร์ในขณะที่อยู่ในforลูปช่วง และถ้าฉันลบมันออกจากรายการลูปของฉันจะเป็นโมฆะหรือไม่?

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
หากคุณต้องการจินตนาการคุณสามารถใช้เพรดิเคตstd::remove_ifที่ "ทำสิ่งของ" แล้วส่งกลับค่าจริงหากคุณต้องการให้ลบองค์ประกอบออก
Benjamin Lindley

มีเหตุผลไหมที่คุณไม่สามารถเพิ่มตัวนับดัชนีแล้วใช้บางอย่างเช่น inv.erase (ดัชนี)?
TomJ

4
@ TomJ: นั่นยังคงทำให้การทำซ้ำผิดพลาด
Ben Voigt

@ TomJ และมันจะเป็นตัวทำลายประสิทธิภาพ - ในทุก ๆ การลบคุณจะต้องย้ายองค์ประกอบทั้งหมดหลังจากที่ลบไปแล้ว
Ivan

1
@BenVoigt ฉันแนะนำให้เปลี่ยนเป็นstd::listด้านล่าง
bobobobo

คำตอบ:


99

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

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

ตัวอย่างเช่น:

auto i = std::begin(inv);

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

5
จะไม่ใช้สำนวนลบ - ลบที่นี่?
Naveen

4
@Naveen ฉันตัดสินใจที่จะไม่พยายามทำเช่นนั้นเพราะเห็นได้ชัดว่าเขาต้องวนซ้ำทุกรายการทำการคำนวณกับมันจากนั้นอาจลบออกจากคอนเทนเนอร์ Erase-remove บอกว่าคุณเพิ่งลบองค์ประกอบที่เพรดิเคตส่งคืนtrueAFAIU และดูเหมือนว่าวิธีนี้จะดีกว่าที่จะไม่ผสมตรรกะการวนซ้ำกับเพรดิเคต
Seth Carnegie

4
@SethCarnegie ลบ - ลบด้วยแลมบ์ดาสำหรับเพรดิเคตอย่างหรูหราช่วยให้ (เนื่องจากนี่เป็น C ++ 11 แล้ว)
Potatoswatter

11
ไม่ชอบโซลูชันนี้เป็น O (N ^ 2) สำหรับคอนเทนเนอร์ส่วนใหญ่ remove_ifจะดีกว่า.
Ben Voigt

6
คำตอบนี้ถูกต้องeraseส่งคืนตัวทำซ้ำที่ถูกต้องใหม่ อาจไม่มีประสิทธิภาพ แต่รับประกันว่าจะใช้งานได้
sp2danny

56

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

ฟอร์ลูปที่อิงตามช่วงเป็นเพียงน้ำตาลเชิงไวยากรณ์สำหรับลูป "ปกติ" โดยใช้ตัววนซ้ำดังนั้นข้างต้นจึงมีผลบังคับใช้

ดังที่กล่าวมาคุณสามารถ:

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

6
" เพราะมีความเป็นไปได้ว่าเวกเตอร์จัดสรรบล็อกของหน่วยความจำในการที่จะช่วยให้องค์ประกอบของมัน " ไม่มีที่จะไม่จัดสรรเนื่องจากการเรียกไปยังvector eraseสาเหตุที่ตัวทำซ้ำไม่ถูกต้องเนื่องจากแต่ละองค์ประกอบที่ประสบความสำเร็จในการลบองค์ประกอบจะถูกย้าย
ildjarn

2
ค่าเริ่มต้นในการบันทึก[&]จะเหมาะสมเพื่อให้เขา "ทำบางอย่าง" กับตัวแปรท้องถิ่น
Potatoswatter

2
สิ่งนี้ดูไม่ง่ายไปกว่าการวนซ้ำแบบวนซ้ำนอกจากนี้คุณต้องจำไว้ว่าล้อมรอบremove_ifด้วย.eraseมิฉะนั้นจะไม่มีอะไรเกิดขึ้น
bobobobo

3
@bobobobo ถ้าตาม "วนซ้ำตามตัววน" คุณหมายถึงคำตอบของ Seth Carnegieนั่นคือ O (n ^ 2) โดยเฉลี่ย std::remove_ifคือ O (n)
Branko Dimitrijevic

1
@bobobobo ถ้าคุณไม่จำเป็นต้องเข้าถึงโดยสุ่มจริงๆ
Branko Dimitrijevic

16

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

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

โปรดทราบว่าคุณต้องb != v.end()ทำการทดสอบตามที่เป็นอยู่ หากคุณพยายามปรับให้เหมาะสมดังนี้:

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

คุณจะเข้าสู่ UB เนื่องจากคุณeไม่ถูกต้องหลังจากการeraseโทรครั้งแรก


@ildjarn: เออจริง! นั่นเป็นการพิมพ์ผิด
สม่ำเสมอ

2
นี่ไม่ใช่สำนวนการลบ - ลบ ไม่มีการโทรstd::removeและเป็น O (N ^ 2) ไม่ใช่ O (N)
Potatoswatter

1
@ Potatoswatter: ไม่แน่นอน ฉันพยายามชี้ให้เห็นปัญหาในการลบขณะที่คุณทำซ้ำ ฉันเดาว่าถ้อยคำของฉันไม่ผ่านการรวบรวม?
สม่ำเสมอ

5

เป็นข้อกำหนดที่เข้มงวดในการลบองค์ประกอบขณะอยู่ในลูปนั้นหรือไม่? มิฉะนั้นคุณสามารถตั้งค่าพอยน์เตอร์ที่คุณต้องการลบเป็น NULL และส่งผ่านเวกเตอร์อีกครั้งเพื่อลบพอยน์เตอร์ 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);

2

ขออภัยสำหรับการไม่โพสต์ข้อความและขออภัยหากความเชี่ยวชาญ c ++ ของฉันขัดขวางคำตอบของฉัน แต่ถ้าคุณพยายามทำซ้ำในแต่ละรายการและทำการเปลี่ยนแปลงที่เป็นไปได้ (เช่นการลบดัชนี) ให้ลองใช้ backwords เพื่อวนซ้ำ

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

//do stuff
//erase index x

}

เมื่อลบดัชนี x ลูปถัดไปจะเป็นของรายการ "ข้างหน้า" การวนซ้ำครั้งสุดท้าย ฉันหวังว่านี่จะช่วยใครสักคนได้


อย่าลืมเมื่อใช้ x เพื่อเข้าถึงดัชนีบางรายการให้ทำ x-1 lol
lilbigwill99

1

ตกลงฉันมาช้า แต่อย่างไรก็ตาม: ขออภัยไม่ได้แก้ไขสิ่งที่ฉันอ่านจนถึงตอนนี้ - เป็นไปได้คุณต้องมีตัวทำซ้ำสองตัว:

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

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

อย่างไรก็ตามโปรดทราบว่านี่ไม่ใช่เธรดที่ปลอดภัย (!) - อย่างไรก็ตามสิ่งนี้ใช้ได้เช่นกันสำหรับโซลูชันอื่น ๆ ข้างต้น ...


ห่า. นี่คือการโอเวอร์คิล
Kesse

@ เคสเซ่จริงเหรอ? นี่คืออัลกอริทึมที่มีประสิทธิภาพที่สุดที่คุณจะได้รับจากเวกเตอร์ (ไม่ว่าจะเป็นลูปตามช่วงหรือลูปตัววนซ้ำแบบคลาสสิก) วิธีนี้คุณจะย้ายแต่ละองค์ประกอบในเวกเตอร์พร้อมกันมากที่สุดและคุณจะวนซ้ำเวกเตอร์ทั้งหมดในครั้งเดียว คุณจะย้ายองค์ประกอบที่ตามมากี่ครั้งและทำซ้ำบนเวกเตอร์หากคุณลบองค์ประกอบที่ตรงกันแต่ละรายการผ่านerase(หากคุณลบมากกว่าหนึ่งองค์ประกอบแน่นอน)
Aconcagua

1

ฉันจะแสดงด้วยตัวอย่างตัวอย่างด้านล่างนี้ลบองค์ประกอบแปลกออกจากเวกเตอร์:

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;

}

เอาต์พุต aw ด้านล่าง:

024
024
024

โปรดทราบว่าเมธอดeraseจะส่งคืนตัวทำซ้ำถัดไปของตัวทำซ้ำที่ผ่านไป

จากที่นี่เราสามารถใช้วิธีการสร้างเพิ่มเติม:

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

ดูวิธีการใช้งานstd::remove_ifได้ที่นี่ https://en.cppreference.com/w/cpp/algorithm/remove


1

ตรงข้ามกับชื่อกระทู้นี้ฉันจะใช้บัตรสองใบ:

#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

วิธีแก้ปัญหาที่หรูหรากว่านั้นคือการเปลี่ยนไปใช้std::list(สมมติว่าคุณไม่ต้องการการเข้าถึงแบบสุ่มที่รวดเร็ว)

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

จากนั้นคุณสามารถลบด้วย.remove_ifและ C ++ functor ในหนึ่งบรรทัด:

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

ฉันแค่เขียน functor ที่ยอมรับอาร์กิวเมนต์เดียว (the Widget*) ค่าที่ส่งคืนคือเงื่อนไขในการลบ a ออกWidget*จากรายการ

ฉันพบว่าไวยากรณ์นี้ถูกต้อง ฉันไม่คิดว่าฉันจะใช้remove_ifสำหรับstd :: vectors - มีมากinv.begin()และinv.end()มีเสียงรบกวนที่นั่นคุณอาจจะดีกว่าถ้าใช้การลบตามดัชนีจำนวนเต็มหรือเพียงแค่ลบตามตัววนซ้ำปกติธรรมดา (ดังที่แสดง ด้านล่าง) แต่คุณไม่ควรลบออกจากตรงกลางเป็นstd::vectorอย่างมากดังนั้นขอแนะนำให้เปลี่ยนไปใช้ a listสำหรับกรณีของการลบกลางรายการบ่อยๆ

อย่างไรก็ตามโปรดทราบว่าฉันไม่ได้มีโอกาสเรียกร้องdeleteให้Widget*ผู้ที่ถูกลบออกไป ในการทำเช่นนั้นจะมีลักษณะดังนี้:

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

คุณยังสามารถใช้วนซ้ำตามปกติได้เช่น:

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

หากคุณไม่ชอบความยาวfor( list<Widget*>::iterator iter = widgets.begin() ; ...คุณสามารถใช้

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

1
ฉันไม่คิดว่าคุณเข้าใจวิธีremove_ifการstd::vectorทำงานจริงและวิธีการรักษาความซับซ้อนของ O (N)
Ben Voigt

นั่นไม่สำคัญ การลบออกจากตรงกลางของ a std::vectorจะเลื่อนแต่ละองค์ประกอบหลังจากที่คุณลบออกไปเสมอทำให้เป็นstd::listทางเลือกที่ดีกว่ามาก
bobobobo

1
ไม่มันจะไม่ "เลื่อนแต่ละองค์ประกอบขึ้นมา" remove_ifจะเลื่อนแต่ละองค์ประกอบขึ้นตามจำนวนช่องว่างที่ว่าง ตามเวลาที่คุณบัญชีสำหรับการใช้งานแคชremove_ifในการกำจัดมีประสิทธิภาพเหนือกว่าน่าจะเกิดจากstd::vector std::listและถนอมอาหารO(1)เข้าถึงแบบสุ่ม
Ben Voigt

1
จากนั้นคุณจะมีคำตอบที่ดีในการค้นหาคำถาม คำถามนี้พูดถึงการทำรายการซ้ำซึ่งก็คือ O (N) สำหรับคอนเทนเนอร์ทั้งสอง และลบองค์ประกอบ O (N) ซึ่งก็คือ O (N) สำหรับทั้งสองคอนเทนเนอร์
Ben Voigt

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

0

ฉันคิดว่าฉันจะทำสิ่งต่อไปนี้ ...

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

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

วิธีแก้ไข: 1) ถ่ายสำเนาของเวกเตอร์ต้นฉบับ 2) ทำซ้ำตัววนซ้ำโดยใช้สำเนานี้ 2) ทำบางอย่างและลบออกจากเวกเตอร์ดั้งเดิม

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

การลบองค์ประกอบทีละรายการจะนำไปสู่ประสิทธิภาพของ N ^ 2 ได้อย่างง่ายดาย ดีกว่าที่จะทำเครื่องหมายองค์ประกอบที่ควรลบและลบออกพร้อมกันหลังจากวนซ้ำ ถ้าฉันอาจถือว่า nullptr เป็นองค์ประกอบที่ไม่ถูกต้องในเวกเตอร์ของคุณ

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

ควรทำงาน.

ในกรณีที่ "Do some stuff" ของคุณไม่ได้เปลี่ยนองค์ประกอบของเวกเตอร์และใช้ในการตัดสินใจลบหรือเก็บองค์ประกอบไว้เท่านั้นคุณสามารถแปลงเป็น lambda (ตามที่แนะนำในโพสต์ก่อนหน้าของใครบางคน) และใช้

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