Дозволити ітерацію внутрішнього вектора без протікання реалізації


32

У мене є клас, який представляє список людей.

class AddressBook
{
public:
  AddressBook();

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

Я хочу дозволити клієнтам перебирати вектор людей. Перша думка у мене була просто:

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

Однак я не хочу просочувати деталі реалізації клієнту . Я, можливо, захочу підтримувати певні інваріанти, коли вектор модифікований, і я втрачаю контроль над цими інваріантами, коли витікаю реалізацію.

Який найкращий спосіб дозволити ітерацію, не протікаючи внутрішні?


2
Перш за все, якщо ви хочете зберегти контроль, вам слід повернути свій вектор у якості посилання const. Ви все одно розкриєте деталі про реалізацію таким чином, тому рекомендую зробити свій клас ітерабельним і ніколи не виставляти структуру даних (можливо, це буде завтра хеш-таблиця?).
ідобі

Швидкий пошук у Google виявив мені такий приклад: sourcemaking.com/design_patterns/Iterator/cpp/1
Doc Brown

1
Що говорить @DocBrown - це, ймовірно, відповідне рішення - на практиці це означає, що ви даєте своєму класу AddressBook метод start () та end () (плюс const перевантаження та, зрештою, також cbegin / cend), який просто повертає вектору початок () та кінець ( ). Роблячи це, ваш клас також буде корисним для всіх найбільш std алгоритмів.
stijn

1
@stijn Це має бути відповідь, а не коментар :-)
Філіп Кендалл

1
@stijn Ні, про це не говорить DocBrown та пов'язана стаття. Правильним рішенням є використання проксі-класу, що вказує на клас контейнера, а також безпечний механізм вказівки положення. Повертаючись вектор - х begin()і end()небезпечний тим , що (1) цих типів векторних ітератори (класи) , який запобігає один від переходу в інший контейнер , такі як set. (2) Якщо вектор модифікований (наприклад, вирощений або стираються деякі елементи), деякі або всі векторні ітератори можуть бути визнані недійсними.
rwong

Відповіді:


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 до питання: додавання сюди додаткової обгортки / проксі навколо ітераторів вектора зробить клієнтів незалежними від фактичного основного ітератора
stijn

Крім того, зауважте, що надання a begin()і end()що просто вперед до вектора begin()і end()дозволяє користувачеві змінювати елементи у самому векторі, можливо, використовуючи std::sort(). Залежно від того, які інваріанти ви намагаєтеся зберегти, це може бути, а може бути і не прийнятним. Забезпечення begin()та end(), хоча, є необхідним для підтримки циклів на основі C ++ 11 для циклів.
Патрік Нідзельскі

Ймовірно, ви також повинні показувати той самий код, використовуючи функцію auto, як і типи повернення функцій ітератора при використанні C ++ 14.
Клайм

Як це приховує деталі реалізації?
BЈович

@ BЈовић, не виставляючи повного вектора - приховування не означає, що реалізація повинна бути буквально прихована від заголовка та
поміщена

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я можу стверджувати, що це все-таки варто робити, як (а) чому ні; це всього лише два знаки, (б) мені подобається говорити про те, що я маю на увазі, принаймні, з const(в) це запобігає випадковому вставленню кудись не 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) для кожного методу, який проходить через нього. Від того, чи багато це буде накладним для будь-якої ситуації, залежить від тестування / профілювання, і в багатьох випадках це, мабуть, прекрасно, але це абсолютно неправда - і я вважаю досить безвідповідальною - проголосити, що вона не має накладних витрат.
підкреслюй_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?)
підкреслюється
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.