Permitir iteração de um vetor interno sem vazar a implementação

32

Eu tenho uma classe que representa uma lista de pessoas.

class AddressBook
{
public:
  AddressBook();

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

Eu quero permitir que os clientes iterem sobre o vetor de pessoas. O primeiro pensamento que tive foi simplesmente:

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

No entanto, não quero vazar os detalhes de implementação para o cliente . Talvez eu queira manter certos invariantes quando o vetor é modificado e perco o controle sobre esses invariantes quando vazo a implementação.

Qual é a melhor maneira de permitir a iteração sem vazar os internos?

Elegant Codeworks
fonte
2
Primeiro de tudo, se você quiser manter o controle, retorne seu vetor como uma referência const. Você ainda expôs os detalhes da implementação dessa maneira, por isso recomendo tornar sua classe iterável e nunca expor sua estrutura de dados (talvez seja uma tabela de hash amanhã?).
Idby
Uma rápida pesquisa no google me revelou este exemplo: sourcemaking.com/design_patterns/Iterator/cpp/1
Doc Brown
1
O que o @DocBrown diz é provavelmente a solução apropriada - na prática, isso significa que você fornece à classe AddressBook um método begin () e end () (mais sobrecargas const e, eventualmente, também cbegin / cend), que simplesmente retornam o vetor begin () e end ( ) Ao fazer isso, sua classe também será utilizável por todos os algoritmos mais comuns.
6285 stjn
1
@stijn Isso deve ser uma resposta, não um comentário :-)
Philip Kendall
1
@stijn Não, não é isso que DocBrown e o artigo vinculado dizem. A solução correta é usar uma classe de proxy apontando para a classe de contêiner junto com um mecanismo seguro para indicar a posição. Retornar os vetores begin()e end()é perigoso porque (1) esses tipos são iteradores de vetores (classes) que impedem que um alterne para outro contêiner, como a set. (2) Se o vetor for modificado (por exemplo, crescido ou alguns itens apagados), alguns ou todos os iteradores do vetor poderão ter sido invalidados.
Rdong

Respostas:

25

permitir a iteração sem vazar os internos é exatamente o que o padrão do iterador promete. Claro que isso é principalmente teoria, então aqui está um exemplo prático:

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

Você fornece padrões begine endmétodos, assim como seqüências no STL e os implementa simplesmente encaminhando para o método do vetor. Isso vaza alguns detalhes da implementação, ou seja, você está retornando um iterador de vetor, mas nenhum cliente sensato jamais deve depender disso, portanto isso não é uma preocupação. Eu mostrei todas as sobrecargas aqui, mas é claro que você pode começar apenas fornecendo a versão const se os clientes não puderem alterar nenhuma entrada de Pessoas. O uso da nomeação padrão traz benefícios: quem lê o código imediatamente sabe que ele fornece iteração 'padrão' e, como tal, trabalha com todos os algoritmos comuns, com base em intervalos para loops etc.

stijn
fonte
Nota: embora este certamente funciona e é aceito vale a pena tomar nota de de rwong comentários à pergunta: adicionando iterators um invólucro adicional / proxy do vetor por aqui faria clientes independente do real subjacente iterador
Stijn
Além disso, nota que o fornecimento de um begin()e end()que apenas para a frente para o vetor do begin()e end()permite que o usuário modificar os elementos do próprio vetor, talvez usando std::sort(). Dependendo dos invariantes que você está tentando preservar, isso pode ou não ser aceitável. Fornecer begin()e end(), no entanto, é necessário dar suporte ao C ++ 11 baseado em intervalo para loops.
Patrick Niedzielski
Você provavelmente também deve mostrar o mesmo código usando auto como tipos de retorno de funções do iterador ao usar o C ++ 14.
Klaim
Como isso está ocultando os detalhes da implementação?
BЈовић
@ BЈовић por não expor o vector completo - esconderijo não significa necessariamente que a implementação tem de ser literalmente escondido de um cabeçalho e colocar no arquivo de origem: se é cliente privado não pode acessá-lo de qualquer maneira
stijn
4

Se a iteração é tudo que você precisa, talvez um wrapper std::for_eachseja suficiente:

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
fonte
Provavelmente seria melhor aplicar uma consteração com cbegin / cend. Mas essa solução é muito melhor do que dar acesso ao contêiner subjacente.
galop1n
@ galop1n Ele faz impor uma constiteração. O for_each()é uma constfunção de membro. Portanto, o membro peopleé visto como const. Portanto, begin()e end()irá sobrecarregar como const. Portanto, eles retornarão const_iterators para people. Portanto, f()receberá um People const&. Escrevendo cbegin()/ cend()aqui vai mudar nada, na prática, embora como um usuário obsessivo de consteu poderia argumentar que ainda vale a pena fazer, como (a) por que não; é apenas 2 caracteres, (b) I como dizer o que quero dizer, pelo menos, com const, (c) que protege contra acidentalmente colar em algum lugar não const, etc.
underscore_d
3

Você pode usar o idioma pimpl e fornecer métodos para iterar sobre o contêiner.

No cabeçalho:

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

Na fonte:

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

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

Dessa forma, se seu cliente usar o typedef do cabeçalho, ele não perceberá que tipo de contêiner você está usando. E os detalhes da implementação estão completamente ocultos.

BЈовић
fonte
1
Isso é CORRETO ... oculto completo da implementação e sem custos adicionais.
Abstração é tudo.
2
@Abstractioniseverything. " nenhuma sobrecarga adicional " é claramente falsa. O PImpl adiciona uma alocação dinâmica de memória (e, posteriormente, livre) para todas as instâncias, e uma indireção indireta (pelo menos 1) para todos os métodos que passam por ela. Se isso é uma sobrecarga para uma determinada situação depende de benchmarking / criação de perfil e, em muitos casos, provavelmente está perfeitamente bem, mas não é absolutamente verdade - e acho irresponsável - proclamar que não sobrecarga.
underscore_d
@underscore_d Concordo; não querendo ser irresponsável lá, mas acho que fui vítima do contexto. “Nenhuma sobrecarga adicional ...” está tecnicamente incorreta, como você apontou habilmente; desculpas ...
Abstração é tudo.
1

Pode-se fornecer funções-membro:

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

Que permitem o acesso sem expor detalhes da implementação (como contiguidade) e os utilizam em uma classe de iterador:

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

Os iteradores podem ser retornados pelo catálogo de endereços da seguinte maneira:

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

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

Você provavelmente precisaria detalhar a classe do iterador com características etc., mas acho que isso fará o que você pediu.

jbcoe
fonte
1

se você deseja uma implementação exata das funções do std :: vector, use a herança privada como abaixo e controle o que está exposto.

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

Edit: Isso não é recomendado se você também deseja ocultar a estrutura de dados interna, isto é, std :: vector

Ayub
fonte
A herança em tal situação é, na melhor das hipóteses, muito preguiçosa (você deve usar a composição e fornecer métodos de encaminhamento, especialmente porque há muito pouco para encaminhar aqui), muitas vezes confusos e inconvenientes (e se você quiser adicionar seus próprios métodos que conflitam com vectoroutros, que você nunca deseja usar, mas deve herdar?) e talvez ativamente perigoso (e se a classe herdada preguiçosamente puder ser excluída por meio de um ponteiro para esse tipo de base em algum lugar, mas [irresponsável] não protegeu contra a destruição de um derivado obj através tais ponteiro um, assim simplesmente destruir é UB)?
underscore_d