วิธีลบออกจากแผนที่ในขณะที่วนซ้ำ?


177

ฉันจะลบออกจากแผนที่ในขณะที่วนซ้ำได้อย่างไร ชอบ:

std::map<K, V> map;
for(auto i : map)
    if(needs_removing(i))
        // remove it from the map

ถ้าฉันใช้map.eraseมันจะทำให้ตัววนซ้ำใช้ไม่ได้


ที่คล้ายกัน: stackoverflow.com/questions/1038708/…
Christian Ammer

คล้ายกันมากขึ้น: stackoverflow.com/questions/800955/…
Kleist


ในหัวข้อเดียวกัน: stackoverflow.com/questions/263945/…
Agostino

คำตอบ:


280

การเชื่อมโยงคอนเทนเนอร์มาตรฐานลบสำนวน:

for (auto it = m.cbegin(); it != m.cend() /* not hoisted */; /* no increment */)
{
  if (must_delete)
  {
    m.erase(it++);    // or "it = m.erase(it)" since C++11
  }
  else
  {
    ++it;
  }
}

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

แก้ไข Pre-C ++ 11 คุณไม่สามารถลบผู้ทำซ้ำ ที่นั่นคุณจะต้องพูดว่า:

for (std::map<K,V>::iterator it = m.begin(); it != m.end(); ) { /* ... */ }

การลบองค์ประกอบออกจากคอนเทนเนอร์ไม่ได้ขัดแย้งกับความคงตัวขององค์ประกอบ โดยการเปรียบเทียบมันถูกต้องตามกฎหมายอย่างสมบูรณ์แบบเสมอกับdelete pที่pเป็นตัวชี้ไปยังคงที่ Constness ไม่ จำกัด อายุการใช้งาน ค่า const ใน C ++ ยังคงสามารถหยุดอยู่ได้


1
"ไม่เปิดเผยภาชนะภายในตัวลูปด้วย" คุณหมายถึงอะไร
Dani

2
@Dani: for (int i = 0; i < v.size(); i++)ดีคมชัดนี้การก่อสร้างในศตวรรษที่ ที่นี่เราต้องพูดv[i]ในวงคือเราจะต้องพูดถึงภาชนะ RBFL ในอีกทางหนึ่งแนะนำตัวแปรลูปที่ใช้งานได้โดยตรงเป็นค่าและไม่จำเป็นต้องมีความรู้เกี่ยวกับคอนเทนเนอร์ภายในลูป นี่เป็นเงื่อนงำการใช้ RBFL สำหรับลูปที่ไม่จำเป็นต้องรู้เกี่ยวกับคอนเทนเนอร์ การลบเป็นสถานการณ์ตรงกันข้ามที่สมบูรณ์ซึ่งทุกอย่างเกี่ยวกับคอนเทนเนอร์
Kerrek SB

3
@skyhisi: แน่นอน นี่คือหนึ่งในการใช้งานที่ถูกต้องตามกฎหมายของการเพิ่มโพสต์: เพิ่มขึ้นครั้งแรกที่itจะได้รับ iterator ต่อไปถูกต้องแล้วลบเก่า มันไม่ทำงานในทางกลับกัน!
Kerrek SB

5
ฉันอ่านบางแห่งว่าใน C ++ 11 it = v.erase(it);ตอนนี้ใช้ได้กับแผนที่ด้วยนั่นคือลบ () ในองค์ประกอบที่เชื่อมโยงทั้งหมดตอนนี้ส่งคืนตัววนซ้ำถัดไป ดังนั้นจึงไม่จำเป็นต้องใช้ kludge แบบเก่าที่เพิ่มโพสต์เพิ่มขึ้นภายในการลบ () สิ่งนี้ (ถ้าเป็นจริง) เป็นสิ่งที่ดีเนื่องจาก kludge อาศัยเวทมนตร์เวรที่โพสต์เพิ่มขึ้นภายในฟังก์ชั่นการโทร "คงที่" โดยผู้ดูแลรักษามือใหม่ที่จะเพิ่มขึ้นจากการเรียกใช้ฟังก์ชันหรือเพื่อสลับ ไป preincrement "เพราะนั่นเป็นเพียงสิ่งสไตล์" ฯลฯ
Dewi มอร์แกน

3
ทำไมคุณถึงโทรit++เข้าif และ elseบล็อก? มันจะไม่เพียงพอที่จะเรียกมันอีกครั้งหลังจากนี้?
nburk

25

โดยส่วนตัวแล้วฉันชอบรูปแบบนี้ที่ชัดเจนและเรียบง่ายขึ้นเล็กน้อยโดยมีค่าใช้จ่ายของตัวแปรพิเศษ:

for (auto it = m.cbegin(), next_it = it; it != m.cend(); it = next_it)
{
  ++next_it;
  if (must_delete)
  {
    m.erase(it);
  }
}

ข้อดีของวิธีนี้:

  • สำหรับห่วงเพิ่มขึ้นทำให้รู้สึกว่าเป็นการเพิ่ม;
  • การดำเนินการลบเป็นการลบแบบง่าย ๆ แทนที่จะผสมกับตรรกะที่เพิ่มขึ้น
  • หลังจากบรรทัดแรกของลูปเนื้อหาความหมายitและnext_itยังคงอยู่ตลอดการวนซ้ำทำให้คุณสามารถเพิ่มคำสั่งเพิ่มเติมที่อ้างถึงได้อย่างง่ายดายโดยไม่ต้อง headcratching ว่าจะทำงานได้ตามที่ต้องการหรือไม่ยกเว้นว่าคุณไม่สามารถใช้งานได้itหลังจากลบทิ้ง) .

2
ฉันสามารถคิดถึงข้อดีอีกประการหนึ่งได้จริง ๆ ถ้าลูปเรียกใช้โค้ดที่ลบรายการนั้นซ้ำไปซ้ำมาหรือก่อนหน้า (และลูปไม่รู้ตัว) มันจะทำงานได้โดยไม่ทำอันตรายใด ๆ ข้อ จำกัด เพียงอย่างเดียวคือไม่ว่าจะมีบางสิ่งที่ลบสิ่งที่ชี้ไปโดย next_it หรือผู้สืบทอด รายการ / แผนที่ที่ถูกล้างทั้งหมดสามารถทดสอบได้เช่นกัน
Larswad

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

1
โปรดจำไว้ว่าใครก็ตามที่ใช้คำตอบนี้: คุณต้องมี "++ next_it" อยู่ในลูป for และไม่ใช่นิพจน์การวนซ้ำ หากคุณพยายามที่จะย้ายมันลงในนิพจน์การทำซ้ำเป็น "it = next_it ++" จากนั้นในการทำซ้ำครั้งล่าสุดเมื่อ "มัน" จะถูกตั้งค่าเท่ากับ "m.cend ()" คุณจะพยายามทำซ้ำ "next_it" ที่ผ่านมา "m.cend ()" ซึ่งผิดพลาด
cdgraham

6

โดยย่อ "ฉันจะลบออกจากแผนที่ขณะทำซ้ำได้อย่างไร"

  • ด้วย impl map แบบเก่า: คุณทำไม่ได้
  • ด้วย impl map ใหม่: เกือบจะเป็น @KerrekSB ที่แนะนำ แต่มีปัญหาทางไวยากรณ์บางอย่างในสิ่งที่เขาโพสต์

จากแผนที่ GCC impl (หมายเหตุGXX_EXPERIMENTAL_CXX0X ):

#ifdef __GXX_EXPERIMENTAL_CXX0X__
      // _GLIBCXX_RESOLVE_LIB_DEFECTS
      // DR 130. Associative erase should return an iterator.
      /**
       *  @brief Erases an element from a %map.
       *  @param  position  An iterator pointing to the element to be erased.
       *  @return An iterator pointing to the element immediately following
       *          @a position prior to the element being erased. If no such 
       *          element exists, end() is returned.
       *
       *  This function erases an element, pointed to by the given
       *  iterator, from a %map.  Note that this function only erases
       *  the element, and that if the element is itself a pointer,
       *  the pointed-to memory is not touched in any way.  Managing
       *  the pointer is the user's responsibility.
       */
      iterator
      erase(iterator __position)
      { return _M_t.erase(__position); }
#else
      /**
       *  @brief Erases an element from a %map.
       *  @param  position  An iterator pointing to the element to be erased.
       *
       *  This function erases an element, pointed to by the given
       *  iterator, from a %map.  Note that this function only erases
       *  the element, and that if the element is itself a pointer,
       *  the pointed-to memory is not touched in any way.  Managing
       *  the pointer is the user's responsibility.
       */
      void
      erase(iterator __position)
      { _M_t.erase(__position); }
#endif

ตัวอย่างที่มีสไตล์เก่าและใหม่:

#include <iostream>
#include <map>
#include <vector>
#include <algorithm>

using namespace std;
typedef map<int, int> t_myMap;
typedef vector<t_myMap::key_type>  t_myVec;

int main() {

    cout << "main() ENTRY" << endl;

    t_myMap mi;
    mi.insert(t_myMap::value_type(1,1));
    mi.insert(t_myMap::value_type(2,1));
    mi.insert(t_myMap::value_type(3,1));
    mi.insert(t_myMap::value_type(4,1));
    mi.insert(t_myMap::value_type(5,1));
    mi.insert(t_myMap::value_type(6,1));

    cout << "Init" << endl;
    for(t_myMap::const_iterator i = mi.begin(); i != mi.end(); i++)
        cout << '\t' << i->first << '-' << i->second << endl;

    t_myVec markedForDeath;

    for (t_myMap::const_iterator it = mi.begin(); it != mi.end() ; it++)
        if (it->first > 2 && it->first < 5)
            markedForDeath.push_back(it->first);

    for(size_t i = 0; i < markedForDeath.size(); i++)
        // old erase, returns void...
        mi.erase(markedForDeath[i]);

    cout << "after old style erase of 3 & 4.." << endl;
    for(t_myMap::const_iterator i = mi.begin(); i != mi.end(); i++)
        cout << '\t' << i->first << '-' << i->second << endl;

    for (auto it = mi.begin(); it != mi.end(); ) {
        if (it->first == 5)
            // new erase() that returns iter..
            it = mi.erase(it);
        else
            ++it;
    }

    cout << "after new style erase of 5" << endl;
    // new cend/cbegin and lambda..
    for_each(mi.cbegin(), mi.cend(), [](t_myMap::const_reference it){cout << '\t' << it.first << '-' << it.second << endl;});

    return 0;
}

พิมพ์:

main() ENTRY
Init
        1-1
        2-1
        3-1
        4-1
        5-1
        6-1
after old style erase of 3 & 4..
        1-1
        2-1
        5-1
        6-1
after new style erase of 5
        1-1
        2-1
        6-1

Process returned 0 (0x0)   execution time : 0.021 s
Press any key to continue.

1
ฉันไม่เข้าใจ มีปัญหาmi.erase(it++);อะไร?
lvella

1
@lvella ดูสหกรณ์ "ถ้าฉันใช้ map.erase มันจะทำให้ตัววนซ้ำใช้ไม่ได้"
Kashyap

วิธีการใหม่ของคุณจะไม่ทำงานหากหลังจากลบแผนที่จะว่างเปล่า ในกรณีนั้นตัววนซ้ำจะถูกยกเลิก ดังนั้นเร็ว ๆ if(mi.empty()) break;นี้หลังจากการลบดีกว่าที่จะแทรก
Rahat Zaman

4

ร่าง C ++ 20 มีฟังก์ชันอำนวยความสะดวก std::erase_ifร่างมีฟังก์ชั่นอำนวยความสะดวก

ดังนั้นคุณสามารถใช้ฟังก์ชั่นนั้นเป็นซับเดียวได้

std::map<K, V> map_obj;
//calls needs_removing for each element and erases it, if true was reuturned
std::erase_if(map_obj,needs_removing);
//if you need to pass only part of the key/value pair
std::erase_if(map_obj,[](auto& kv){return needs_removing(kv.first);});

3

เศร้ามากใช่มั้ย วิธีที่ฉันมักจะทำคือสร้างคอนเทนเนอร์ของตัววนซ้ำแทนการลบระหว่างการสำรวจเส้นทาง จากนั้นวนซ้ำผ่านคอนเทนเนอร์และใช้ map.erase ()

std::map<K,V> map;
std::list< std::map<K,V>::iterator > iteratorList;

for(auto i : map ){
    if ( needs_removing(i)){
        iteratorList.push_back(i);
    }
}
for(auto i : iteratorList){
    map.erase(*i)
}

แต่หลังจากที่ลบไปแล้วส่วนที่เหลือจะไม่ถูกต้อง
Dani


@Dani: ไม่ได้อยู่ในแผนที่ การลบในแผนที่จะทำให้ตัววนซ้ำใช้ไม่ได้กับรายการที่ถูกลบ
ลุงเบ็น

3

สมมติว่า C ++ 11 ต่อไปนี้เป็นตัววนลูปแบบหนึ่งซับถ้าสิ่งนี้สอดคล้องกับสไตล์การเขียนโปรแกรมของคุณ:

using Map = std::map<K,V>;
Map map;

// Erase members that satisfy needs_removing(itr)
for (Map::const_iterator itr = map.cbegin() ; itr != map.cend() ; )
  itr = needs_removing(itr) ? map.erase(itr) : std::next(itr);

การเปลี่ยนแปลงสไตล์รองอื่น ๆ สองสามรายการ:

  • แสดงประเภทประกาศ ( Map::const_iterator) เมื่อเป็นไปได้ / สะดวกใช้มากกว่าautoสะดวกมากกว่าการใช้
  • ใช้usingสำหรับประเภทเทมเพลตเพื่อทำให้ประเภทเสริม ( Map::const_iterator) อ่าน / ดูแลง่ายขึ้น
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.