Izinkan iterasi vektor internal tanpa membocorkan implementasi

32

Saya memiliki kelas yang mewakili daftar orang.

class AddressBook
{
public:
  AddressBook();

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

Saya ingin memungkinkan klien untuk beralih pada vektor orang. Pikiran pertama yang saya miliki hanyalah:

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

Namun, saya tidak ingin membocorkan detail implementasi ke klien . Saya mungkin ingin mempertahankan invarian tertentu ketika vektor dimodifikasi, dan saya kehilangan kendali atas invarian ini ketika saya membocorkan implementasinya.

Apa cara terbaik untuk memungkinkan iterasi tanpa membocorkan internal?

Codeworks yang elegan
sumber
2
Pertama-tama, jika Anda ingin mempertahankan kontrol, Anda harus mengembalikan vektor Anda sebagai referensi const. Anda masih akan mengekspos detail implementasi seperti itu, jadi saya sarankan membuat kelas Anda iterable dan tidak pernah mengekspos struktur data Anda (mungkin besok akan menjadi tabel hash?).
idoby
Pencarian google cepat mengungkapkan saya contoh ini: sourcemaking.com/design_patterns/Iterator/cpp/1
Doc Brown
1
Apa yang dikatakan @DocBrown kemungkinan adalah solusi yang tepat - dalam praktiknya ini berarti Anda memberikan metode AddressBook class Anda begin () dan end () (ditambah overloads const dan akhirnya juga cbegin / cend) yang hanya mengembalikan vektor begin () dan end ( ). Dengan melakukannya, kelas Anda juga akan dapat digunakan oleh semua algorythms paling std.
stijn
1
@stijn Itu seharusnya menjadi jawaban, bukan komentar :-)
Philip Kendall
1
@stijn Tidak, bukan itu yang dikatakan DocBrown dan artikel yang ditautkan. Solusi yang benar adalah dengan menggunakan kelas proxy yang menunjuk ke kelas wadah bersama dengan mekanisme yang aman untuk menunjukkan posisi. Mengembalikan vektor begin()dan end()berbahaya karena (1) jenis tersebut adalah vektor iterator (kelas) yang mencegah seseorang beralih ke wadah lain seperti a set. (2) Jika vektornya diubah (misal item yang ditanam atau terhapus), beberapa atau semua iterator vektor bisa saja tidak valid.
rwong

Jawaban:

25

memungkinkan iterasi tanpa membocorkan internal adalah persis apa yang dijanjikan pola iterator. Tentu saja itu terutama teori jadi di sini adalah contoh praktis:

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

Anda memberikan standar begindan endmetode, seperti urutan dalam STL dan menerapkannya hanya dengan meneruskan ke metode vektor. Ini tidak membocorkan beberapa detail implementasi yaitu bahwa Anda mengembalikan vektor iterator tetapi tidak ada klien waras yang harus bergantung pada hal itu sehingga itu juga bukan masalah. Saya telah menunjukkan semua kelebihan di sini, tetapi tentu saja Anda bisa mulai dengan hanya menyediakan versi const jika klien tidak dapat mengubah entri People. Menggunakan penamaan standar memiliki manfaat: siapa pun yang membaca kode segera tahu itu memberikan iterasi 'standar' dan karena itu bekerja dengan semua algoritma umum, rentang berdasarkan untuk loop dll.

stijn
sumber
catatan: meskipun ini pasti bekerja dan diterima, ada baiknya mencatat komentar rwong untuk pertanyaan: menambahkan pembungkus / proxy tambahan di sekitar iterator vektor di sini akan membuat klien independen dari iterator yang mendasarinya sebenarnya
stijn
Selain itu, perhatikan bahwa menyediakan a begin()dan end()yang hanya meneruskan ke vektor begin()dan end()memungkinkan pengguna untuk memodifikasi elemen dalam vektor itu sendiri, mungkin menggunakan std::sort(). Bergantung pada invarian apa yang ingin Anda pertahankan, ini mungkin atau mungkin tidak dapat diterima. Menyediakan begin()dan end(), bagaimanapun, diperlukan untuk mendukung rentang berbasis C ++ 11 untuk loop.
Patrick Niedzielski
Anda mungkin juga harus menunjukkan kode yang sama menggunakan otomatis sebagai jenis pengembalian fungsi iterator saat menggunakan C ++ 14.
Klaim
Bagaimana ini menyembunyikan detail implementasi?
BЈовић
@ BЈовић dengan tidak mengekspos vektor lengkap - bersembunyi tidak berarti implementasi harus benar-benar disembunyikan dari header dan dimasukkan ke dalam file sumber: jika klien pribadi tidak dapat mengaksesnya
stijn
4

Jika iterasi adalah semua yang Anda butuhkan, maka mungkin pembungkus std::for_eachsudah cukup:

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;
};
catscradle
sumber
Mungkin akan lebih baik untuk menegakkan konstelasi dengan cbegin / cend. Tetapi solusi itu jauh lebih baik daripada memberikan akses ke wadah yang mendasarinya.
galop1n
@ galop1n Ini tidak menegakkan constiterasi. Ini for_each()adalah constfungsi anggota. Karenanya, anggota peopletersebut dianggap sebagai const. Karenanya, begin()dan end()akan membebani sebagai const. Karenanya, mereka akan kembali const_iteratorke people. Karenanya, f()akan menerima a People const&. Menulis cbegin()/ di cend()sini tidak akan mengubah apa pun, dalam praktiknya, meskipun sebagai pengguna obsesif constsaya mungkin berpendapat itu masih layak dilakukan, karena (a) mengapa tidak; itu hanya 2 karakter, (b) Saya suka mengatakan apa yang saya maksud, paling tidak dengan const, (c) itu menjaga agar tidak sengaja menempel di suatu tempat yang bukan const, dll.
underscore_d
3

Anda dapat menggunakan idiom jerawat , dan memberikan metode untuk beralih di atas wadah.

Di tajuk:

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

Dalam sumber:

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

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

Dengan cara ini, jika klien Anda menggunakan typedef dari header, mereka tidak akan melihat jenis wadah yang Anda gunakan. Dan detail implementasi sepenuhnya tersembunyi.

BЈовић
sumber
1
Ini BENAR ... selesaikan penyembunyian implementasi dan tidak ada overhead tambahan.
Abstraksi adalah segalanya.
2
@Abstractioniseverything. " tidak ada overhead tambahan " jelas salah. PImpl menambahkan alokasi memori dinamis (dan, kemudian, gratis) untuk setiap instance, dan tipuan pointer (setidaknya 1) untuk setiap metode yang melewatinya. Apakah itu terlalu mahal untuk situasi tertentu tergantung pada benchmark / profiling, dan dalam banyak kasus itu mungkin baik-baik saja, tetapi sama sekali tidak benar - dan saya pikir agak tidak bertanggung jawab - untuk menyatakan bahwa itu tidak memiliki overhead.
underscore_d
@underscore_d Saya setuju; tidak bermaksud tidak bertanggung jawab di sana, tetapi, saya kira saya menjadi korban konteksnya. "Tidak ada overhead tambahan ..." secara teknis salah, seperti yang Anda tunjukkan dengan cekatan; permintaan maaf ...
Abstraksi adalah segalanya.
1

Satu dapat menyediakan fungsi anggota:

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

Yang memungkinkan akses tanpa memaparkan detail implementasi (seperti kedekatan) dan menggunakannya dalam kelas iterator:

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

Iterator kemudian dapat dikembalikan oleh buku alamat sebagai berikut:

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

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

Anda mungkin perlu menyempurnakan kelas iterator dengan sifat-sifat dll, tetapi saya pikir ini akan melakukan apa yang Anda minta.

jbcoe
sumber
1

jika Anda ingin implementasi fungsi yang tepat dari std :: vector, gunakan warisan pribadi seperti di bawah ini dan kontrol apa yang terbuka.

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

Sunting: Ini tidak direkomendasikan jika Anda juga ingin menyembunyikan struktur data internal yaitu std :: vector

Ayub
sumber
Warisan dalam situasi seperti itu paling baik sangat malas (Anda harus menggunakan komposisi dan memberikan metode penerusan, terutama karena ada sedikit untuk meneruskan di sini), sering membingungkan dan tidak nyaman (bagaimana jika Anda ingin menambahkan metode Anda sendiri yang bertentangan dengan vectoryang, yang Anda tidak pernah ingin gunakan tetapi tetap harus mewarisi?), dan mungkin secara aktif berbahaya (bagaimana jika kelas yang diwarisi dari malas dapat dihapus melalui pointer ke jenis pangkalan di suatu tempat, tetapi [tidak bertanggung jawab] tidak melindungi terhadap penghancuran objek yang diturunkan melalui penunjuk seperti itu, sehingga dengan mudah menghancurkannya adalah UB?)
underscore_d