Autoriser l'itération d'un vecteur interne sans fuite de l'implémentation

32

J'ai une classe qui représente une liste de personnes.

class AddressBook
{
public:
  AddressBook();

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

Je veux permettre aux clients d'itérer sur le vecteur des personnes. La première pensée que j'ai eue était simplement:

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

Cependant, je ne veux pas divulguer les détails de l'implémentation au client . Je peux vouloir conserver certains invariants lorsque le vecteur est modifié, et je perds le contrôle de ces invariants lorsque je fuit l'implémentation.

Quelle est la meilleure façon de permettre l'itération sans fuir les internes?

Codeworks élégant
la source
2
Tout d'abord, si vous voulez garder le contrôle, vous devez renvoyer votre vecteur comme référence const. Vous exposeriez toujours les détails d'implémentation de cette façon, donc je recommande de rendre votre classe itérable et de ne jamais exposer votre structure de données (ce sera peut-être une table de hachage demain?)
idobie
Une recherche rapide sur Google m'a révélé cet exemple: sourcemaking.com/design_patterns/Iterator/cpp/1
Doc Brown
1
Ce que dit @DocBrown est probablement la solution appropriée - en pratique, cela signifie que vous donnez à votre classe AddressBook une méthode begin () et end () (plus des surcharges const et éventuellement aussi cbegin / cend) qui renvoient simplement le début () et la fin du vecteur ( ). Ce faisant, votre classe sera également utilisable par la plupart des algorithmes standard.
stijn
1
@stijn Cela devrait être une réponse, pas un commentaire :-)
Philip Kendall
1
@stijn Non, ce n'est pas ce que dit DocBrown et l'article lié. La bonne solution consiste à utiliser une classe proxy pointant vers la classe conteneur avec un mécanisme sûr pour indiquer la position. Renvoyer les vecteurs begin()et end()sont dangereux car (1) ces types sont des itérateurs de vecteurs (classes) qui empêchent l'un de basculer vers un autre conteneur tel que a set. (2) Si le vecteur est modifié (par exemple, développé ou certains éléments effacés), certains ou tous les itérateurs de vecteur pourraient avoir été invalidés.
rwong

Réponses:

25

permettre l'itération sans fuite des internes est exactement ce que le modèle d'itérateur promet. Bien sûr, c'est principalement de la théorie, alors voici un exemple pratique:

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

Vous fournissez des standards beginet des endméthodes, tout comme les séquences dans la STL et les implémentez simplement en les transmettant à la méthode du vecteur. Cela laisse fuir certains détails d'implémentation, à savoir que vous retournez un itérateur vectoriel, mais aucun client sensé ne devrait jamais en dépendre, donc ce n'est pas un problème. J'ai montré toutes les surcharges ici, mais bien sûr, vous pouvez commencer par fournir la version const si les clients ne doivent pas pouvoir modifier les entrées People. L'utilisation de la dénomination standard présente des avantages: toute personne lisant le code sait immédiatement qu'il fournit une itération `` standard '' et, en tant que tel, fonctionne avec tous les algorithmes courants, la plage basée sur les boucles, etc.

stijn
la source
Remarque: bien que cela fonctionne et soit accepté, cela vaut la peine de prendre note des commentaires de rwong à la question: l'ajout d'un wrapper / proxy supplémentaire autour des itérateurs de vecteur rendrait les clients indépendants de l'itérateur sous-jacent réel
stijn
De plus, notez que fournir un begin()et end()qui se transmet simplement aux vecteurs begin()et end()permet à l'utilisateur de modifier les éléments dans le vecteur lui-même, peut-être en utilisant std::sort(). Selon les invariants que vous essayez de conserver, cela peut être acceptable ou non. Fournir begin()et end(), cependant, est nécessaire pour prendre en charge les boucles basées sur la plage C ++ 11.
Patrick Niedzielski
Vous devriez probablement également afficher le même code en utilisant auto comme types de retour des fonctions d'itérateur lorsque vous utilisez C ++ 14.
Klaim
Comment cela cache-t-il les détails de la mise en œuvre?
BЈовић
@ BЈовић en n'exposant pas le vecteur complet - masquer ne signifie pas nécessairement que l'implémentation doit être littéralement cachée d'un en-tête et placée dans le fichier source: si son client privé n'y accède pas de toute façon
stijn
4

Si l'itération est tout ce dont vous avez besoin, alors peut-être qu'un wrapper std::for_eachsuffirait:

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;
};
Le berceau du chat
la source
Il serait probablement préférable d'imposer une itération const avec cbegin / cend. Mais cette solution est de loin meilleure que de donner accès au conteneur sous-jacent.
galop1n
@ galop1n Il fait appliquer une constitération. Le for_each()est une constfonction membre. Par conséquent, le membre peopleest considéré comme const. Par conséquent, begin()et end()surchargera comme const. Par conséquent, ils renverront const_iterators à people. Par conséquent, f()recevra un People const&. Écrire cbegin()/ cend()ici ne changera rien, dans la pratique, mais en tant qu'utilisateur obsessionnel de constje pourrais dire que cela vaut toujours la peine, comme (a) pourquoi pas; c'est juste 2 caractères, (b) j'aime dire ce que je veux dire, au moins avec const, (c) ça protège contre un collage accidentel quelque part non const, etc.
underscore_d
3

Vous pouvez utiliser l'idiome pimpl et fournir des méthodes pour parcourir le conteneur.

Dans l'en-tête:

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

Dans la source:

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

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

De cette façon, si votre client utilise le typedef de l'en-tête, il ne remarquera pas le type de conteneur que vous utilisez. Et les détails de mise en œuvre sont complètement cachés.

BЈовић
la source
1
C'est CORRECT ... la mise en œuvre complète se cache et aucun frais supplémentaire.
abstraction est tout.
2
@Abstractioniseverything. " pas de frais généraux supplémentaires " est manifestement faux. PImpl ajoute une allocation de mémoire dynamique (et, plus tard, gratuite) pour chaque instance, et une indirection de pointeur (au moins 1) pour chaque méthode qui la traverse. La question de savoir si cela représente beaucoup de frais généraux pour une situation donnée dépend de l'analyse comparative / du profilage, et dans de nombreux cas, c'est probablement parfaitement bien, mais il n'est absolument pas vrai - et je pense plutôt irresponsable - de proclamer qu'il n'y a pas de frais généraux.
underscore_d
@underscore_d Je suis d'accord; ne signifie pas être irresponsable là-bas, mais, je suppose que je suis tombé en proie au contexte. "Pas de frais généraux supplémentaires ..." est techniquement incorrect, comme vous l'avez habilement souligné; excuses ...
abstraction est tout.
1

On pourrait fournir des fonctions membres:

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

Qui permettent l'accès sans exposer les détails d'implémentation (comme la contiguïté) et les utilisent au sein d'une classe d'itérateur:

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

Les itérateurs peuvent ensuite être retournés par le carnet d'adresses comme suit:

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

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

Vous auriez probablement besoin d'étoffer la classe des itérateurs avec des traits, etc. mais je pense que cela fera ce que vous avez demandé.

jbcoe
la source
1

si vous voulez une implémentation exacte des fonctions de std :: vector, utilisez l'héritage privé comme ci-dessous et contrôlez ce qui est exposé.

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

Modifier: Ceci n'est pas recommandé si vous souhaitez également masquer la structure de données interne, c'est-à-dire std :: vector

Ayub
la source
L'héritage dans une telle situation est au mieux très paresseux (vous devez utiliser la composition et fournir des méthodes de transfert, d'autant plus qu'il y en a si peu à transmettre ici), souvent déroutant et peu pratique (que se passe-t-il si vous souhaitez ajouter vos propres méthodes qui entrent en conflit avec les vectorautres, que vous ne voulez jamais utiliser mais que vous devez néanmoins hériter?), et peut-être activement dangereux (et si la classe héritée paresseusement pouvait être supprimée par un pointeur vers ce type de base quelque part, mais elle [irresponsablement] ne protégeait pas contre la destruction de un obj dérivé via un tel pointeur, donc simplement le détruire est UB?)
underscore_d