¿Por qué se utilizarían clases anidadas en C++?
¿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?
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::list
o std::map
todos 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
.
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 SomeSpecificCollection
que agregue objetos de clase Element
. Entonces puedes:
declarar dos clases:
SomeSpecificCollection
yElement
- malo, porque el nombre "Elemento" es lo suficientemente general como para provocar un posible conflicto de nombresintroduzca un espacio de nombres
someSpecificCollection
y declare clasessomeSpecificCollection::Collection
ysomeSpecificCollection::Element
. No hay riesgo de conflicto de nombres, pero ¿puede ser más detallado?declarar dos clases globales
SomeSpecificCollection
ySomeSpecificCollectionElement
, lo cual tiene inconvenientes menores, pero probablemente esté bien.declarar clase global
SomeSpecificCollection
y claseElement
como 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
SomeSpecificCollection
usted se refiere simplemente aElement
, y en todos los demás lugares comoSomeSpecificCollection::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
SomeSpecificCollection
tambié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 .cpp
y nunca incluirla en .h
.
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 Field
clase genérica que tiene un número de identificación, un código de tipo y un nombre de campo. Si quiero buscar uno vector
de estos Field
mensajes 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 Field
mensajes de correo electrónico puede usar el match
ámbito dentro de la Field
propia clase:
vector<Field>::const_iterator it = find_if(fields.begin(), fields.end(), Field::match("FieldName"));