Permitir la iteración de un vector interno sin filtrar la implementación

32

Tengo una clase que representa una lista de personas.

class AddressBook
{
public:
  AddressBook();

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

Quiero permitir que los clientes repitan el vector de las personas. El primer pensamiento que tuve fue simplemente:

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

Sin embargo, no quiero filtrar los detalles de implementación al cliente . Es posible que desee mantener ciertos invariantes cuando se modifica el vector, y pierdo el control sobre estos invariantes cuando pierdo la implementación.

¿Cuál es la mejor manera de permitir la iteración sin filtrar las partes internas?

Codeworks elegante
fuente
2
En primer lugar, si desea mantener el control, debe devolver su vector como referencia constante. Todavía expondría los detalles de implementación de esa manera, por lo que recomiendo hacer que su clase sea iterable y nunca exponer su estructura de datos (¿tal vez será una tabla hash mañana?).
idoby
Una búsqueda rápida en Google me reveló este ejemplo: sourcemaking.com/design_patterns/Iterator/cpp/1
Doc Brown
1
Lo que dice @DocBrown es probablemente la solución adecuada: en la práctica, esto significa que le da a su clase de AddressBook un método begin () y end () (más sobrecargas constantes y eventualmente también cbegin / cend) que simplemente devuelve el vector begin () y end ( ) Al hacerlo, su clase también podrá ser utilizada por la mayoría de los algoritmos estándar.
stijn
1
@stijn Eso debería ser una respuesta, no un comentario :-)
Philip Kendall
1
@stijn No, eso no es lo que dice DocBrown y el artículo vinculado. La solución correcta es usar una clase proxy que apunte a la clase contenedor junto con un mecanismo seguro para indicar la posición. Devolver los vectores begin()y end()son peligrosos porque (1) esos tipos son iteradores de vectores (clases) que evitan que uno cambie a otro contenedor como a set. (2) Si se modifica el vector (p. Ej., Crecido o algunos elementos borrados), algunos o todos los iteradores del vector podrían haberse invalidado.
rwong

Respuestas:

25

Permitir la iteración sin filtrar las partes internas es exactamente lo que promete el patrón iterador. Por supuesto, eso es principalmente teoría, así que aquí hay un ejemplo práctico:

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

Usted proporciona estándares beginy endmétodos, al igual que las secuencias en el STL, y los implementa simplemente reenviando al método vectorial Esto filtra algunos detalles de implementación, es decir, que está devolviendo un iterador de vectores, pero ningún cliente sensato debería depender de eso, por lo que no es una preocupación. He mostrado todas las sobrecargas aquí, pero por supuesto, puede comenzar simplemente proporcionando la versión constante si los clientes no pueden cambiar ninguna entrada de Personas. El uso de la nomenclatura estándar tiene beneficios: cualquiera que lea el código inmediatamente sabe que proporciona una iteración 'estándar' y, como tal, funciona con todos los algoritmos comunes, rango basado en bucles, etc.

stijn
fuente
nota: aunque esto ciertamente funciona y se acepta, vale la pena tomar nota de los comentarios de rwong a la pregunta: agregar un envoltorio / proxy adicional alrededor de los iteradores del vector aquí haría que los clientes sean independientes del iterador subyacente real
stijn
Además, tenga en cuenta que proporcionar un begin()y end()que simplemente reenviar a los vectores begin()y end()permite al usuario modificar los elementos en el vector en sí, tal vez utilizando std::sort(). Dependiendo de qué invariantes esté tratando de preservar, esto puede o no ser aceptable. Proporcionar begin()y end(), sin embargo, es necesario para admitir el C ++ 11 basado en rango para bucles.
Patrick Niedzielski
Probablemente también debería mostrar el mismo código usando auto como tipos de retorno de funciones de iterador cuando usa C ++ 14.
Klaim
¿Cómo oculta esto los detalles de implementación?
Bћовић
@ BЈовић al no exponer el vector completo: ocultar no necesariamente significa que la implementación tiene que estar literalmente oculta de un encabezado y ponerla en el archivo fuente: si es un cliente privado, no puede acceder a ella de todos modos
hasta el
4

Si la iteración es todo lo que necesita, entonces quizás sea std::for_eachsuficiente un envoltorio :

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;
};
cuna
fuente
Probablemente sería mejor hacer cumplir una iteración constante con cbegin / cend. Pero esa solución es mucho mejor que dar acceso al contenedor subyacente.
galop1n
@ galop1n Se hace cumplir una constiteración. El for_each()es una constfunción miembro. Por lo tanto, el miembro peoplees visto como const. Por lo tanto, begin()y end()se sobrecargará como const. Por lo tanto, volverán a const_iterators people. Por lo tanto, f()recibirá a People const&. Escribir cbegin()/ cend()aquí no cambiará nada, en la práctica, aunque como usuario obsesivo de constYo podría argumentar que todavía vale la pena hacerlo, como (a) por qué no; es sólo 2 caracteres, (b) I como decir lo que quiero decir, al menos con const, (c) que protege contra accidentalmente pegar alguna parte no const, etc.
underscore_d
3

Puede usar el idioma pimpl y proporcionar métodos para iterar sobre el contenedor.

En el encabezado:

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

En la fuente:

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

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

De esta manera, si su cliente utiliza el typedef del encabezado, no notará qué tipo de contenedor está utilizando. Y los detalles de implementación están completamente ocultos.

BЈовић
fuente
1
Esto es CORRECTO ... ocultación de implementación completa y sin gastos adicionales
abstracción lo es todo.
2
@Abstractioniseverything. " sin sobrecarga adicional " es simplemente falso. PImpl agrega una asignación de memoria dinámica (y, más tarde, gratuita) para cada instancia, y una indirección de puntero (al menos 1) para cada método que lo atraviesa. Ya sea que es mucho por encima de cualquier situación dada depende de la evaluación comparativa / perfiles, y en muchos casos es probable que sea perfectamente bien, pero no es absolutamente cierto - y creo que más bien irresponsable - de anunciar que no tiene ninguna sobrecarga.
underscore_d
@underscore_d Estoy de acuerdo; no significa ser irresponsable allí, pero supongo que caí presa del contexto. "Sin sobrecarga adicional ..." es técnicamente incorrecto, como usted señaló hábilmente; disculpas ...
abstracción lo es todo.
1

Se podrían proporcionar funciones miembro:

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

Lo que permite el acceso sin exponer detalles de implementación (como contigüidad) y usarlos dentro de una clase 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_;
    }
};

Los iteradores pueden ser devueltos por la libreta de direcciones de la siguiente manera:

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

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

Probablemente necesites completar la clase de iterador con rasgos, etc., pero creo que esto hará lo que has pedido.

jbcoe
fuente
1

si desea la implementación exacta de funciones de std :: vector, use la herencia privada como se muestra a continuación y controle lo que está expuesto.

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

Editar: Esto no se recomienda si también desea ocultar la estructura de datos interna, es decir, std :: vector

Ayub
fuente
La herencia en tal situación es, en el mejor de los casos, muy perezosa (debe usar la composición y proporcionar métodos de reenvío, especialmente porque hay muy pocos para reenviar aquí), a menudo confusos e inconvenientes (¿qué pasa si desea agregar sus propios métodos que entran en conflicto con los vectordemás? que nunca quiere usar, pero que de todos modos debe heredar?), y tal vez sea activamente peligroso (¿y si la clase de la que se hereda perezosamente podría eliminarse mediante un puntero a ese tipo base en alguna parte, pero [irresponsablemente] no protegió contra la destrucción de un obj derivado a través de dicho puntero, ¿así que simplemente destruirlo es UB?)
underscore_d