อนุญาตการวนซ้ำของเวกเตอร์ภายในโดยไม่มีการรั่วไหลของการใช้งาน


32

ฉันมีชั้นเรียนที่แสดงถึงรายชื่อของผู้คน

class AddressBook
{
public:
  AddressBook();

private:
  std::vector<People> people;
}

ฉันต้องการอนุญาตให้ลูกค้าทำซ้ำเวกเตอร์ของผู้คน ความคิดแรกที่ฉันมีก็คือ:

std::vector<People> & getPeople { return people; }

แต่ฉันไม่ต้องการที่จะรั่วไหลรายละเอียดการดำเนินการให้กับลูกค้า ฉันอาจต้องการรักษาค่าคงที่บางอย่างเมื่อมีการแก้ไขเวกเตอร์และฉันสูญเสียการควบคุมค่าคงที่เหล่านี้เมื่อฉันรั่วการใช้งาน

วิธีที่ดีที่สุดในการอนุญาตให้ทำซ้ำโดยไม่รั่วไหลภายในคืออะไร?


2
ก่อนอื่นถ้าคุณต้องการรักษาการควบคุมคุณควรคืนค่าเวกเตอร์ของคุณเป็นการอ้างอิงแบบ const คุณจะยังคงเปิดเผยรายละเอียดการใช้งานด้วยวิธีนี้ดังนั้นฉันขอแนะนำให้ทำให้คลาสของคุณทำซ้ำได้และไม่เคยเปิดเผยโครงสร้างข้อมูลของคุณ (อาจจะเป็นตารางแฮชในวันพรุ่งนี้?)
idoby

การค้นหา google อย่างรวดเร็วทำให้ฉันเห็นตัวอย่างนี้: sourcemaking.com/design_patterns/Iterator/cpp/1
Doc Brown

1
สิ่งที่ @DocBrown พูดว่าน่าจะเป็นทางออกที่เหมาะสม - ในทางปฏิบัตินั่นหมายความว่าคุณให้คลาส AddressBook ของคุณด้วยวิธีเริ่มต้น () และ end () รวมทั้งโอเวอร์โหลดจำนวนมากและในที่สุดก็ใช้ cbegin / cend ด้วยเช่นกัน ) โดยการทำเช่นนี้ในชั้นเรียนของคุณจะสามารถใช้งานได้โดย algorythms มาตรฐานทั้งหมด
Stijn

1
@stijn ที่ควรจะเป็นคำตอบที่ไม่ได้แสดงความคิดเห็น :-)
ฟิลิปเคนดัลล์

1
@stijn ไม่นั่นไม่ใช่สิ่งที่ DocBrown และบทความที่เชื่อมโยงพูดถึง วิธีแก้ไขที่ถูกต้องคือใช้คลาสพร็อกซีที่ชี้ไปที่คลาสคอนเทนเนอร์พร้อมกับกลไกที่ปลอดภัยเพื่อระบุตำแหน่ง กลับมาของเวกเตอร์begin()และend()เป็นอันตรายเพราะ (1) ผู้ประเภท iterators เวกเตอร์ (เรียน) setซึ่งจะช่วยป้องกันจากการเปลี่ยนมาใช้ภาชนะอื่นเช่น (2) หากมีการแก้ไขเวกเตอร์ (เช่นโตขึ้นหรือลบบางรายการ) เวกเตอร์ตัววนซ้ำบางส่วนหรือทั้งหมดอาจไม่ถูกต้อง
ร. ว.

คำตอบ:


25

อนุญาตให้ทำซ้ำโดยไม่รั่วภายในเป็นสิ่งที่รูปแบบตัววนซ้ำสัญญา แน่นอนว่าส่วนใหญ่เป็นทฤษฎีดังนั้นนี่คือตัวอย่างที่ใช้งานได้จริง:

class AddressBook
{
  using peoples_t = std::vector<People>;
public:
  using iterator = peoples_t::iterator;
  using const_iterator = peoples_t::const_iterator;

  AddressBook();

  iterator begin() { return people.begin(); }
  iterator end() { return people.end(); }
  const_iterator begin() const { return people.begin(); }
  const_iterator end() const { return people.end(); }
  const_iterator cbegin() const { return people.cbegin(); }
  const_iterator cend() const { return people.cend(); }

private:
  peoples_t people;
};

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


หมายเหตุ: แม้ว่าจะใช้งานได้จริงและเป็นที่ยอมรับก็ควรทราบว่าการแสดงความคิดเห็นของ rwong ในคำถาม: การเพิ่ม wrapper / proxy พิเศษรอบตัววนซ้ำของเวกเตอร์ที่นี่จะทำให้ลูกค้าเป็นอิสระจาก iterator พื้นฐานจริง
stijn

นอกจากนี้ทราบว่าการให้บริการbegin()และend()ที่เพิ่งไปข้างหน้าเพื่อเวกเตอร์begin()และช่วยให้ผู้ใช้ในการปรับเปลี่ยนองค์ประกอบในเวกเตอร์ของตัวเองอาจจะใช้end() std::sort()ขึ้นอยู่กับสิ่งที่คุณพยายามรักษาค่าคงที่สิ่งนี้อาจหรืออาจไม่เป็นที่ยอมรับ การจัดหาbegin()และend()จำเป็นต้องสนับสนุนช่วง C ++ 11 สำหรับลูป
Patrick Niedzielski

คุณควรแสดงรหัสเดียวกันโดยใช้ auto เป็นชนิด return ของฟังก์ชันตัววนซ้ำเมื่อใช้ C ++ 14
Klaim

วิธีนี้ซ่อนรายละเอียดการใช้งานได้อย่างไร
BЈовић

@ BЈовићโดยไม่เปิดเผยเวกเตอร์ที่สมบูรณ์ - การซ่อนไม่จำเป็นต้องหมายความว่าการติดตั้งจะต้องซ่อนตัวจากส่วนหัวและใส่ในไฟล์ต้นฉบับ: ถ้าไคลเอ็นต์ส่วนตัวไม่สามารถเข้าถึงได้
stijn

4

หากการวนซ้ำเป็นสิ่งที่คุณต้องการบางทีการล้อมรอบstd::for_eachอาจพอเพียง:

class AddressBook
{
public:
  AddressBook();

  template <class F>
  void for_each(F f) const
  {
    std::for_each(begin(people), end(people), f);
  }

private:
  std::vector<People> people;
};

มันอาจจะดีกว่าที่จะบังคับใช้การทำซ้ำ const ด้วย cbegin / cend แต่ทางออกนั้นดีกว่าให้การเข้าถึงคอนเทนเนอร์พื้นฐาน
galop1n

@ galop1n มันไม่บังคับใช้constซ้ำ for_each()เป็นconstฟังก์ชั่นสมาชิก ดังนั้นสมาชิกถูกมองว่าเป็นpeople constดังนั้นbegin()และจะเกินเป็นend() constดังนั้นพวกเขาจะกลับมาเพื่อconst_iterator peopleดังนั้นจะได้รับf() People const&การเขียนcbegin()/ cend()ที่นี่จะไม่เปลี่ยนแปลงอะไรเลยในทางปฏิบัติแม้ว่าในฐานะผู้ใช้ที่มีความคิดครอบงำของconstฉันอาจจะโต้แย้งว่า มันเป็นแค่ 2 chars (b) ฉันชอบพูดในสิ่งที่ฉันหมายถึงอย่างน้อยก็ด้วยconst(c) มันจะป้องกันการวางโดยไม่ตั้งใจในบางแห่งที่ไม่ใช่ - constฯลฯ
underscore_d

3

คุณสามารถใช้pimpl สำนวนและให้วิธีการย้ำมากกว่าภาชนะ

ในส่วนหัว:

typedef People* PeopleIt;

class AddressBook
{
public:
  AddressBook();


  PeopleIt begin();
  PeopleIt begin() const;
  PeopleIt end();
  PeopleIt end() const;

private:
  struct Imp;
  std::unique_ptr<Imp> pimpl;
};

ในแหล่งที่มา:

struct AddressBook::Imp
{
  std::vector<People> people;
};

PeopleIt AddressBook::begin()
{
  return &pimpl->people[0];
}

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


1
นี่คือการแก้ไข ... การซ่อนการใช้งานอย่างสมบูรณ์และไม่มีค่าใช้จ่ายเพิ่มเติม
สิ่งที่เป็นนามธรรมทุกอย่าง

2
@Abstractioniseverything " ไม่มีค่าใช้จ่ายเพิ่มเติม " เป็นเท็จ PImpl เพิ่มการจัดสรรหน่วยความจำแบบไดนามิก (และหลังจากนั้นฟรี) สำหรับทุก ๆ อินสแตนซ์และการเปลี่ยนทิศทางตัวชี้ (อย่างน้อย 1) สำหรับทุกวิธีที่ผ่าน ไม่ว่าจะเป็นค่าใช้จ่ายมากสำหรับสถานการณ์ใดก็ตามขึ้นอยู่กับการเปรียบเทียบ / การทำโปรไฟล์และในหลาย ๆ กรณีมันอาจจะดีอย่างสมบูรณ์ แต่ก็ไม่เป็นความจริง - และฉันคิดว่าค่อนข้างขาดความรับผิดชอบ - เพื่อประกาศว่าไม่มีค่าใช้จ่าย
underscore_d

@underscore_d ฉันเห็นด้วย; ไม่ได้หมายความว่าจะไม่รับผิดชอบที่นั่น แต่ฉันคิดว่าฉันตกเป็นเหยื่อของบริบท “ ไม่มีค่าใช้จ่ายเพิ่มเติม ... ” เป็นเทคนิคที่ไม่ถูกต้องตามที่คุณชี้ให้เห็น ขอโทษ ...
นามธรรมคือทุกสิ่ง

1

หนึ่งสามารถให้ฟังก์ชั่นสมาชิก:

size_t Count() const
People& Get(size_t i)

ซึ่งอนุญาตการเข้าถึงโดยไม่เปิดเผยรายละเอียดการใช้งาน (เช่นความต่อเนื่อง) และใช้สิ่งเหล่านี้ภายในคลาสตัววนซ้ำ:

class Iterator
{
    AddressBook* addressBook_;
    size_t index_;

public:
    Iterator(AddressBook& addressBook, size_t index=0) 
    : addressBook_(&addressBook), index_(index) {}

    People& operator*()
    {
        return addressBook_->Get(index_);
    }

    Iterator& operator ++ ()
    {
       ++index_;
       return *this;
    }

    bool operator != (const Iterator& i) const
    {
        assert(addressBook_ == i.addressBook_);
        return index_ != i.index_;
    }
};

ผู้ทำซ้ำสามารถส่งคืนโดยสมุดรายชื่อดังนี้:

AddressBook::Iterator AddressBook::begin()
{
    return Iterator(this);
}

AddressBook::Iterator AddressBook::end()
{
    return Iterator(this, Count());
}

คุณอาจต้องทำให้คลาสตัววนซ้ำมีลักษณะอื่น ๆ แต่ฉันคิดว่าสิ่งนี้จะทำในสิ่งที่คุณถาม


1

หากคุณต้องการใช้งานฟังก์ชั่นที่แน่นอนจาก std :: vector ให้ใช้การสืบทอดส่วนตัวดังต่อไปนี้และควบคุมสิ่งที่เปิดเผย

template <typename T>
class myvec : private std::vector<T>
{
public:
    using std::vector<T>::begin;
    using std::vector<T>::end;
    using std::vector<T>::push_back;
};

แก้ไข: สิ่งนี้ไม่แนะนำถ้าคุณต้องการซ่อนโครงสร้างข้อมูลภายในเช่น std :: vector


มรดกในสถานการณ์ดังกล่าวเป็นที่ที่ดีที่สุดที่ขี้เกียจมาก (คุณควรจะใช้องค์ประกอบและให้ส่งต่อวิธีการโดยเฉพาะอย่างยิ่งตั้งแต่มีไม่กี่ดังนั้นเพื่อส่งต่อที่นี่) มักจะทำให้เกิดความสับสนและไม่สะดวก (สิ่งที่ถ้าคุณต้องการที่จะเพิ่มวิธีการของคุณเองที่ขัดแย้งกับvectorคนที่ สิ่งที่คุณไม่ต้องการใช้ แต่อย่างไรก็ตามจะต้องรับช่วง?) และอาจเป็นอันตราย (ถ้าหากคลาสที่รับการสืบทอดอย่างเกียจคร้านอาจถูกลบผ่านตัวชี้ไปยังประเภทฐานนั้นที่ไหนสักแห่ง แต่มัน [ไม่รับผิดชอบ] ไม่ได้ป้องกันการทำลาย obj ที่ได้รับผ่านตัวชี้เช่นนั้นเพียงแค่ทำลายมันคือ UB?)
underscore_d
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.