Consentire l'iterazione di un vettore interno senza perdere l'implementazione


32

Ho una classe che rappresenta un elenco di persone.

class AddressBook
{
public:
  AddressBook();

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

Voglio consentire ai clienti di scorrere sul vettore delle persone. Il primo pensiero che ho avuto è stato semplicemente:

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

Tuttavia, non voglio far trapelare i dettagli dell'implementazione al client . Potrei voler mantenere determinati invarianti quando il vettore viene modificato e perdo l'implementazione perdendo il controllo su questi invarianti.

Qual è il modo migliore per consentire l'iterazione senza perdite dagli interni?


2
Prima di tutto, se vuoi mantenere il controllo, dovresti restituire il tuo vettore come riferimento const. Esporresti comunque i dettagli di implementazione in quel modo, quindi ti consiglio di rendere la tua classe ripetibile e di non esporre mai la tua struttura di dati (forse sarà una tabella hash domani?).
idoby

Una rapida ricerca su Google mi ha rivelato questo esempio: sourcemaking.com/design_patterns/Iterator/cpp/1
Doc Brown

1
Quella che dice @DocBrown è probabilmente la soluzione appropriata - in pratica ciò significa che dai alla tua classe AddressBook un metodo begin () e end () (oltre a sovraccarichi const e infine anche cbegin / cend) che semplicemente restituiscono il vettore begin () e end ( ). In questo modo la tua classe sarà utilizzabile anche da tutti gli algoritmi più comuni.
stijn,

1
@stijn Questa dovrebbe essere una risposta, non un commento :-)
Philip Kendall,

1
@stijn No, non è quello che dice DocBrown e l'articolo collegato. La soluzione corretta è utilizzare una classe proxy che punta alla classe contenitore insieme a un meccanismo sicuro per indicare la posizione. Restituire i vettori begin()e end()sono pericolosi perché (1) quei tipi sono iteratori di vettori (classi) che impediscono a uno di passare a un altro contenitore come a set. (2) Se il vettore viene modificato (ad es. Cresciuto o alcuni elementi cancellati), alcuni o tutti gli iteratori vettoriali potrebbero essere stati invalidati.
rwong,

Risposte:


25

consentire l'iterazione senza perdite all'interno è esattamente ciò che promette il modello iteratore. Naturalmente questa è principalmente la teoria, quindi ecco un esempio pratico:

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

Fornisci standard begine endmetodi, proprio come le sequenze nell'STL e le implementi semplicemente inoltrando al metodo del vettore. Questo perde alcuni dettagli di implementazione, vale a dire che stai restituendo un iteratore vettoriale, ma nessun client sano dovrebbe mai dipendere da quello, quindi non è un problema. Ho mostrato tutti i sovraccarichi qui, ma ovviamente puoi iniziare semplicemente fornendo la versione const se i client non dovrebbero essere in grado di modificare alcuna voce People. L'uso della denominazione standard ha vantaggi: chiunque legga il codice immediatamente sa che fornisce un'iterazione 'standard' e come tale funziona con tutti gli algoritmi comuni, intervallo basato su loop ecc.


nota: sebbene questo certamente funzioni ed è accettato vale la pena prendere nota dei commenti di rwong alla domanda: l'aggiunta di un wrapper / proxy extra attorno agli iteratori di vettore renderebbe i clienti indipendenti
dall'iteratore

Inoltre, nota che fornire a begin()e end()che si limita a inoltrare al vettore begin()e end()consente all'utente di modificare gli elementi nel vettore stesso, magari usando std::sort(). A seconda di quali invarianti stai cercando di preservare, questo potrebbe essere o non essere accettabile. Fornire begin()e end(), tuttavia, è necessario per supportare la gamma C ++ 11 per loop.
Patrick Niedzielski,

Probabilmente dovresti anche mostrare lo stesso codice usando auto come tipi restituiti di funzioni iteratore quando usi C ++ 14.
Klaim,

In che modo ciò nasconde i dettagli di implementazione?
BЈовић,

@BЈовић non esponendo il vettore completo - nascondersi non significa necessariamente che l'implementazione debba essere letteralmente nascosta da un'intestazione e messa nel file di origine: se il suo client privato non può accedervi comunque
stijn

4

Se l'iterazione è tutto ciò di cui hai bisogno, forse è std::for_eachsufficiente un wrapper :

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

Probabilmente sarebbe meglio imporre una constatazione con cbegin / cend. Ma questa soluzione è di gran lunga migliore rispetto all'accesso al contenitore sottostante.
galop1n,

@ galop1n Si fa applicare constun'iterazione. La for_each()è una constfunzione membro. Quindi, il membro peopleè visto come const. Quindi, begin()e end()sovraccaricherà come const. Quindi, torneranno const_iteratora people. Quindi, f()riceverà a People const&. Scrivere cbegin()/ cend()qui non cambierà nulla, in pratica, anche se come utente ossessivo constpotrei sostenere che valga la pena farlo, come (a) perché no; sono solo 2 caratteri, (b) mi piace dire cosa intendo, almeno con const, (c) protegge da incollaggi accidentali da qualche parte non const, ecc.
underscore_d

3

È possibile utilizzare l'idioma del pimpl e fornire metodi per scorrere il contenitore.

Nell'intestazione:

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

Nella fonte:

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

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

In questo modo, se il client utilizza il typedef dall'intestazione, non noterà il tipo di contenitore che si sta utilizzando. E i dettagli di implementazione sono completamente nascosti.


1
Questo è CORRETTO ... nascondere completamente l'implementazione e nessun costo aggiuntivo.
astrazione è tutto.

2
@Abstractioniseverything. " nessun sovraccarico aggiuntivo " è chiaramente falso. PImpl aggiunge un'allocazione dinamica della memoria (e, successivamente, gratuita) per ogni istanza e un riferimento indiretto del puntatore (almeno 1) per ogni metodo che lo attraversa. Sia che è molto più overhead per ogni situazione dipende da analisi comparativa / profilatura, e in molti casi è probabilmente perfettamente bene, ma non è assolutamente vero - e credo piuttosto irresponsabile - a proclamare che non ha alcun sovraccarico.
underscore_d

@underscore_d Sono d'accordo; non significa essere irresponsabili lì, ma, credo di essere caduto in preda al contesto. "Nessun sovraccarico aggiuntivo ..." è tecnicamente errato, come hai abilmente sottolineato; scuse ...
astrazione è tutto.

1

Si potrebbero fornire funzioni membro:

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

Che consentono l'accesso senza esporre i dettagli dell'implementazione (come la contiguità) e utilizzarli all'interno di una classe iteratore:

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

Gli iteratori possono quindi essere restituiti dalla rubrica come segue:

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

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

Probabilmente avresti bisogno di arricchire la classe di iteratori con tratti ecc. Ma penso che questo farà quello che hai chiesto.


1

se si desidera l'implementazione esatta delle funzioni da std :: vector, utilizzare l'ereditarietà privata come di seguito e controllare ciò che è esposto.

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

Modifica: questo non è consigliato se si desidera nascondere anche la struttura di dati interna, ad esempio std :: vector


L'ereditarietà in una situazione del genere è nella migliore delle ipotesi molto pigra (dovresti usare la composizione e fornire metodi di inoltro, soprattutto perché ci sono così pochi da inoltrare qui), spesso confusa e scomoda (e se vuoi aggiungere i tuoi metodi che sono in conflitto con vectorquelli, che non vorresti mai usare ma che comunque dovresti ereditare?), e forse attivamente pericoloso (e se la classe ereditata pigramente potrebbe essere cancellata da qualche parte con un puntatore a quel tipo di base, ma [irresponsabilmente] non proteggeva dalla distruzione di un obj derivato tramite tale puntatore, quindi semplicemente distruggerlo è UB?)
underscore_d
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.