¿Por qué uno usaría clases anidadas en C ++?

188

¿Alguien puede señalarme algunos buenos recursos para comprender y usar clases anidadas? Tengo algunos materiales 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?

con gafas
fuente
15
Mi consejo para las clases anidadas en C ++ es simplemente no usar clases anidadas.
Billy ONeal
77
Son exactamente como las clases regulares ... excepto anidadas. Úselos cuando la implementación interna de una clase es tan compleja que puede ser modelada más fácilmente por varias clases más pequeñas.
meagar
12
@ Billy: ¿Por qué? Me parece demasiado amplio.
John Dibling
30
Todavía no he visto un argumento por qué las clases anidadas son malas por su naturaleza.
John Dibling
77
@ 7vies: 1. porque simplemente no es necesario: puede hacer lo mismo con clases definidas externamente, lo que reduce el alcance de cualquier variable dada, lo cual es algo bueno. 2. porque puedes hacer todo lo que las clases anidadas pueden hacer con typedef. 3. porque añaden un nivel adicional de sangrado en un ambiente donde evitando largas filas ya es difícil 4. porque está declarando dos objetos separados conceptualmente en una sola classdeclaración, etc.
Billy ONeal

Respuestas:

229

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, también digo que es mía y que puedo cambiarla en cualquier momento para que no pueda usarla.

Mire std::listo std::maptodos contienen clases ocultas (¿o sí?). El punto es que pueden o no, pero debido a que la implementación es privada y oculta, los constructores de STL pudieron actualizar el código sin afectar la forma en que usó el código o dejar un montón de equipaje viejo alrededor del STL porque necesitan para mantener la compatibilidad con algunos tontos que decidieron que querían usar la clase Node que estaba oculta en su interior list.

Martin York
fuente
9
Si está haciendo esto Node, no debería exponerse en el archivo de encabezado.
Billy ONeal
66
@Billy ONeal: ¿Qué sucede si estoy haciendo una implementación de archivo de encabezado como STL o boost?
Martin York
66
@Billy ONeal: No. Es una cuestión de buen diseño, no de opinión. Ponerlo en un espacio de nombres no lo protege del uso. Ahora es parte de la API pública que debe mantenerse a perpetuidad.
Martin York
21
@Billy ONeal: lo protege del uso accidental. También documenta el hecho de que es privado y no debe usarse (no puede usarse a menos que haga algo estúpido). Por lo tanto, no necesita apoyarlo. Ponerlo en un espacio de nombres lo hace parte de la API pública (algo que falta en esta conversación. La API pública significa que debe admitirlo).
Martin York
10
@Billy ONeal: la clase anidada tiene alguna ventaja sobre el espacio de nombres anidado: no puede crear instancias de un espacio de nombres, pero puede crear instancias de una clase. En cuanto a la detailconvención: en lugar de depender de tales convenciones que uno debe tener en cuenta, es mejor depender del compilador que los rastrea por usted.
SasQ
142

Las clases anidadas son como las clases regulares, pero:

  • tienen restricción de acceso adicional (como todas las definiciones dentro de una definición de clase),
  • que no contaminan el espacio de nombres determinado , por ejemplo, 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 la clase B solo sea accesible a través del alcance de la clase A (se denominaría A ::Clase).

Algunos ejemplos:

Clase de anidación pública para ponerla en un ámbito de clase relevante


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

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

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

  3. declara dos clases globales SomeSpecificCollectiony SomeSpecificCollectionElement, que tiene inconvenientes menores, pero probablemente esté bien.

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

    • no se arriesga a ningún conflicto de nombres, ya que Element no está en el espacio de nombres global,
    • en la implementación de SomeSpecificCollectionusted se refiere a solo Element, y en todas partes como SomeSpecificCollection::Element- que se ve + - igual que 3., pero más claro
    • se vuelve simple y simple que es "un elemento de una colección específica", no "un elemento específico de una colección"
    • Es visible que SomeSpecificCollectiontambién es una clase.

En mi opinión, la última variante es definitivamente el diseño más intuitivo y, por lo tanto, el mejor.

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

Introducir otro ámbito dentro de un ámbito de clase


Esto es especialmente útil para introducir typedefs o enumeraciones. Voy a 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
};

Uno entonces llamará:

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

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

Pero si introduce 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, entonces facilita las cosas para los programadores del cliente.

De la misma manera, podría "organizar" un gran grupo de typedefs en una plantilla, si alguna vez tuvo la necesidad de hacerlo. Es un patrón útil a veces.

El idioma de PIMPL


El PIMPL (abreviatura de puntero a IMPLementation) es una expresión útil para eliminar los detalles de implementación de una clase del encabezado. Esto reduce la necesidad de volver a compilar clases según el encabezado de la clase siempre que cambie la parte de "implementación" del encabezado.

Por lo general, 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 simplemente feo (tome WinAPI). Si usa PIMPL, puede incluir cualquier funcionalidad específica de WinAPI solo en .cppy nunca incluirla .h.

Kos
fuente
3
struct Impl; std::auto_ptr<Impl> impl; Este error fue popularizado por Herb Sutter. No use auto_ptr en tipos incompletos, o al menos tome precauciones para evitar la generación de código incorrecto.
Gene Bushuyev
2
@Billy ONeal: Hasta donde yo sé, puede declarar un auto_ptrtipo incompleto en la mayoría de las implementaciones, pero técnicamente es UB a diferencia de algunas de las plantillas en C ++ 0x (por ejemplo unique_ptr) donde se ha hecho explícito que el parámetro de plantilla puede ser un tipo incompleto y donde exactamente el tipo debe estar completo. (por ejemplo, uso de ~unique_ptr)
CB Bailey
2
@Billy ONeal: en C ++ 03 17.4.6.3 [lib.res.on.functions] dice "En particular, los efectos no están definidos en los siguientes casos: [...] si se usa un tipo incompleto como argumento de plantilla al crear instancias de un componente de plantilla ". mientras que en C ++ 0x dice "si se usa un tipo incompleto como argumento de plantilla cuando se crea una instancia de un componente de plantilla, a menos que se permita específicamente para ese componente". y más tarde (por ejemplo): "El parámetro Tde plantilla de unique_ptrpuede ser un tipo incompleto".
CB Bailey
1
@MilesRout Eso es demasiado general. Depende de si el código del cliente puede heredar. Regla: si está seguro de que no eliminará mediante un puntero de clase base, el dtor virtual es completamente redundante.
Kos
2
@IsaacPascual aww, debería actualizar eso ahora que lo tenemos enum class.
Kos
21

No uso muchas clases anidadas, pero las uso de vez en cuando. Especialmente cuando defino algún tipo de tipo de datos, y luego quiero definir un functor 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 deseo buscar uno vectorde estos Fields por número de identificación o nombre, podría construir un functor 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 Fields 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
fuente
Gracias por el gran ejemplo y comentarios, aunque no estoy muy al tanto de las funciones STL. Noto que los constructores en match () son públicos. Supongo que los constructores no siempre deben ser públicos, en cuyo caso no se puede crear una instancia fuera de la clase.
con gafas el
1
@user: en el caso de un functor STL, el constructor debe ser público.
John Dibling
1
@ Billy: Todavía no he visto ningún razonamiento concreto por qué las clases anidadas son malas.
John Dibling
@ John: todas las pautas de estilo de codificación se reducen a una cuestión de opinión. Enumeré varias razones en varios comentarios por aquí, todas las cuales (en mi opinión) son razonables. No hay argumento "factual" que se pueda hacer siempre que el código sea válido y no invoque un comportamiento indefinido. Sin embargo, creo que el ejemplo de código que pones aquí señala una gran razón por la que evito las clases anidadas, es decir, los conflictos de nombres.
Billy ONeal
1
¡Por supuesto, hay razones técnicas para preferir las líneas en línea a las macros!
Miles Rout
14

Se puede implementar un patrón Builder con clase anidada . Especialmente en C ++, personalmente lo encuentro semánticamente más limpio. Por ejemplo:

class Product{
    public:
        class Builder;
}
class Product::Builder {
    // Builder Implementation
}

Más bien que:

class Product {}
class ProductBuilder {}
Yeo
fuente
Claro, funcionará si solo hay una construcción, pero se volverá desagradable si es necesario tener múltiples constructores de concreto. Uno debe tomar cuidadosamente las decisiones de diseño :)
irsis