¿Por qué se utilizarían clases anidadas en C++?

Resuelto bespectacled asked hace 13 años • 6 respuestas

¿Alguien puede indicarme algunos recursos interesantes para comprender y utilizar clases anidadas? Tengo material como Principios de programación y cosas como este IBM Knowledge Center - Clases anidadas

Pero todavía tengo problemas para entender su propósito. ¿Alguien podría ayudarme por favor?

bespectacled avatar Jan 01 '11 00:01 bespectacled
Aceptado

Las clases anidadas son geniales para ocultar detalles de implementación.

Lista:

class List
{
    public:
        List(): head(nullptr), tail(nullptr) {}
    private:
        class Node
        {
              public:
                  int   data;
                  Node* next;
                  Node* prev;
        };
    private:
        Node*     head;
        Node*     tail;
};

Aquí no quiero exponer Node ya que otras personas pueden decidir usar la clase y eso me impediría actualizar mi clase ya que todo lo expuesto es parte de la API pública y debe mantenerse para siempre . Al hacer que la clase sea privada, no solo oculto la implementación, sino que también digo que es mía y que puedo cambiarla en cualquier momento para que no puedas usarla.

Mire std::listo std::maptodos contienen clases ocultas (¿o no?). El punto es que pueden o no, pero debido a que la implementación es privada y oculta, los creadores del STL pudieron actualizar el código sin afectar la forma en que se usó el código, o dejar una gran cantidad de equipaje viejo tirado alrededor del STL porque lo necesitan. para mantener la compatibilidad con versiones anteriores de algún tonto que decidió que quería usar la clase Node que estaba oculta en su interior list.

Martin York avatar Dec 31 '2010 18:12 Martin York

Las clases anidadas son como las clases normales, pero:

  • tienen restricciones de acceso adicionales (como todas las definiciones dentro de una definición de clase),
  • no contaminan el espacio de nombres dado , por ejemplo, el espacio de nombres global. Si cree que la clase B está tan profundamente conectada con la clase A, pero los objetos de A y B no están necesariamente relacionados, entonces es posible que desee que solo se pueda acceder a la clase B a través del alcance de la clase A (se la denominaría A). ::Clase).

Algunos ejemplos:

Clase anidada públicamente para ponerla en el ámbito de una clase relevante


Supongamos que desea tener una clase SomeSpecificCollectionque agregue objetos de clase Element. Entonces puedes:

  1. declarar dos clases: SomeSpecificCollectiony Element- malo, porque el nombre "Elemento" es lo suficientemente general como para provocar un posible conflicto de nombres

  2. introduzca un espacio de nombres someSpecificCollectiony declare clases someSpecificCollection::Collectiony someSpecificCollection::Element. No hay riesgo de conflicto de nombres, pero ¿puede ser más detallado?

  3. declarar dos clases globales SomeSpecificCollectiony SomeSpecificCollectionElement, lo cual tiene inconvenientes menores, pero probablemente esté bien.

  4. declarar clase global SomeSpecificCollectiony clase Elementcomo su clase anidada. Entonces:

    • no corre el riesgo de que haya conflictos de nombres ya que Element no está en el espacio de nombres global,
    • en la implementación de SomeSpecificCollectionusted se refiere simplemente a Element, y en todos los demás lugares como SomeSpecificCollection::Element- que parece +- igual que 3., pero más claro
    • resulta sencillo decir que es "un elemento de una colección específica", no "un elemento específico de una colección"
    • se ve que SomeSpecificCollectiontambién es una clase.

En mi opinión, la última variante es sin duda la más intuitiva y, por tanto, la de mejor diseño.

Permítanme enfatizar: no hay una gran diferencia con respecto a crear dos clases globales con nombres más detallados. Es solo un pequeño detalle, pero en mi humilde opinión hace que el código sea más claro.

Introduciendo otro alcance dentro de un alcance de clase


Esto es especialmente útil para introducir typedefs o enumeraciones. Simplemente publicaré un ejemplo de código aquí:

class Product {
public:
    enum ProductType {
        FANCY, AWESOME, USEFUL
    };
    enum ProductBoxType {
        BOX, BAG, CRATE
    };
    Product(ProductType t, ProductBoxType b, String name);

    // the rest of the class: fields, methods
};

Entonces se llamará:

Product p(Product::FANCY, Product::BOX);

Pero al observar las propuestas de finalización de código para Product::, a menudo se enumeran todos los valores de enumeración posibles (BOX, FANCY, CRATE) y es fácil cometer un error aquí (las enumeraciones fuertemente tipadas de C++0x resuelven eso, pero no importa). ).

Pero si introduces un alcance adicional para esas enumeraciones que usan clases anidadas, las cosas podrían verse así:

class Product {
public:
    struct ProductType {
        enum Enum { FANCY, AWESOME, USEFUL };
    };
    struct ProductBoxType {
        enum Enum { BOX, BAG, CRATE };
    };
    Product(ProductType::Enum t, ProductBoxType::Enum b, String name);

    // the rest of the class: fields, methods
};

Entonces la llamada se ve así:

Product p(Product::ProductType::FANCY, Product::ProductBoxType::BOX);

Luego, al escribir Product::ProductType::un IDE, se obtendrán solo las enumeraciones del alcance deseado sugerido. Esto también reduce el riesgo de cometer un error.

Por supuesto, esto puede no ser necesario para clases pequeñas, pero si uno tiene muchas enumeraciones, facilita las cosas a los programadores del cliente.

De la misma manera, podrías "organizar" una gran cantidad de tipos de letra en una plantilla, si alguna vez fuera necesario. A veces es un patrón útil.

El modismo PIMPL


PIMPL (abreviatura de Pointer to IMPLementation) es un modismo útil para eliminar los detalles de implementación de una clase del encabezado. Esto reduce la necesidad de recompilar clases dependiendo del encabezado de la clase cada vez que cambia la parte de "implementación" del encabezado.

Generalmente se implementa usando una clase anidada:

Xh:

class X {
public:
    X();
    virtual ~X();
    void publicInterface();
    void publicInterface2();
private:
    struct Impl;
    std::unique_ptr<Impl> impl;
}

X.cpp:

#include "X.h"
#include <windows.h>

struct X::Impl {
    HWND hWnd; // this field is a part of the class, but no need to include windows.h in header
    // all private fields, methods go here

    void privateMethod(HWND wnd);
    void privateMethod();
};

X::X() : impl(new Impl()) {
    // ...
}

// and the rest of definitions go here

Esto es particularmente útil si la definición de clase completa necesita la definición de tipos de alguna biblioteca externa que tiene un archivo de encabezado pesado o feo (por ejemplo, WinAPI). Si usa PIMPL, puede incluir cualquier funcionalidad específica de WinAPI solo en .cppy nunca incluirla en .h.

Kos avatar Dec 31 '2010 19:12 Kos

No uso mucho las clases anidadas, pero sí las uso de vez en cuando. Especialmente cuando defino algún tipo de tipo de datos y luego quiero definir un funtor STL diseñado para ese tipo de datos.

Por ejemplo, considere una Fieldclase genérica que tiene un número de identificación, un código de tipo y un nombre de campo. Si quiero buscar uno vectorde estos Fieldmensajes de correo electrónico por número de identificación o nombre, podría construir un funtor para hacerlo:

class Field
{
public:
  unsigned id_;
  string name_;
  unsigned type_;

  class match : public std::unary_function<bool, Field>
  {
  public:
    match(const string& name) : name_(name), has_name_(true) {};
    match(unsigned id) : id_(id), has_id_(true) {};
    bool operator()(const Field& rhs) const
    {
      bool ret = true;
      if( ret && has_id_ ) ret = id_ == rhs.id_;
      if( ret && has_name_ ) ret = name_ == rhs.name_;
      return ret;
    };
    private:
      unsigned id_;
      bool has_id_;
      string name_;
      bool has_name_;
  };
};

Luego, el código que necesita buscar estos Fieldmensajes de correo electrónico puede usar el matchámbito dentro de la Fieldpropia clase:

vector<Field>::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName"));
John Dibling avatar Dec 31 '2010 17:12 John Dibling