Разрешить итерацию внутреннего вектора без утечки реализации


32

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

class AddressBook
{
public:
  AddressBook();

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

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

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

Однако я не хочу передавать детали реализации клиенту . Я могу захотеть сохранить определенные инварианты при изменении вектора, и я теряю контроль над этими инвариантами, когда пропускаю реализацию.

Какой лучший способ разрешить итерацию без утечки внутренних данных?


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

Быстрый поиск в Google показал мне этот пример: sourcemaking.com/design_patterns/Iterator/cpp/1
Док Браун

1
То, что говорит @DocBrown, вероятно, является подходящим решением - на практике это означает, что вы предоставляете классу AddressBook метод begin () и end () (плюс перегрузки const и, в конечном итоге, также cbegin / cend), которые просто возвращают начало begin () и конец вектора ( ). Таким образом, ваш класс также будет использоваться всеми большинством стандартных алгоритмов.
Стийн

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, и реализуете их, просто перенаправляя в метод вектора. Это приводит к утечке некоторых деталей реализации, а именно, что вы возвращаете векторный итератор, но ни один здравомыслящий клиент никогда не должен зависеть от этого, так что это не является проблемой. Я показал все перегрузки здесь, но, конечно, вы можете начать с предоставления константной версии, если клиенты не смогут изменять какие-либо записи сотрудников. Использование стандартного именования имеет свои преимущества: любой, кто читает код, сразу же знает, что он обеспечивает «стандартную» итерацию и, как таковой, работает со всеми распространенными алгоритмами, диапазоном на основе циклов и т. Д.


примечание: хотя это, безусловно, работает и принимается, стоит принять к сведению комментарии rwong к этому вопросу: добавление здесь дополнительной оболочки / прокси вокруг итераторов вектора сделало бы клиентов независимыми от фактического базового итератора
stijn

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

Вы, вероятно, также должны показывать тот же код с использованием auto в качестве возвращаемых типов функций итераторов при использовании 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;
};

Вероятно, было бы лучше применить константную итерацию с помощью cbegin / cend. Но это решение намного лучше, чем предоставление доступа к базовому контейнеру.
galop1n

@ galop1n Это делает исполнение на constитерации. for_each()Является constфункцией - членом. Следовательно, член peopleрассматривается как const. Следовательно begin()и end()перегрузит как const. Следовательно, они вернутся const_iteratorк people. Следовательно, f()получит People const&. Написание cbegin()/ cend()здесь ничего не изменит на практике, хотя, как одержимый пользователь, constя могу утверждать, что это все еще стоит делать, так как (а) почему бы и нет; это всего лишь 2 символа, (б) мне нравится говорить, что я имею в виду, по крайней мере, с 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) для каждого метода, который проходит через него. Является ли это чрезмерными затратами для любой конкретной ситуации, зависит от сравнительного анализа / профилирования, и во многих случаях это, вероятно, совершенно нормально, но это совершенно неверно - и я думаю, что довольно безответственно - провозглашать, что оно не имеет накладных расходов.
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ними, который вы никогда не хотите использовать, но тем не менее должны наследовать?) и, возможно, активно опасны (что, если класс, от которого лениво наследуется, может быть удален через указатель на этот базовый тип где-то, но это [безответственно] не защитит от уничтожения производный объект через такой указатель, так что просто уничтожить его - UB?)
underscore_d
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.