simple - relaciones entre clases c++
¿Cuándo debería usar la herencia privada de C++? (13)
A diferencia de la herencia protegida, la herencia privada de C ++ encontró su camino en el desarrollo de C ++ convencional. Sin embargo, todavía no he encontrado un buen uso para ello.
¿Cuándo lo usan?
A veces me resulta útil usar herencia privada cuando quiero exponer una interfaz más pequeña (por ejemplo, una colección) en la interfaz de otra, donde la implementación de la colección requiere acceso al estado de la clase expositora, de manera similar a las clases internas en Java.
class BigClass;
struct SomeCollection
{
iterator begin();
iterator end();
};
class BigClass : private SomeCollection
{
friend struct SomeCollection;
SomeCollection &GetThings() { return *this; }
};
Entonces, si SomeCollection necesita acceder a BigClass, puede static_cast<BigClass *>(this)
. No es necesario tener un miembro de datos extra ocupando espacio.
A veces podría ser una alternativa a la agregación , por ejemplo, si desea la agregación pero con el comportamiento modificado de la entidad agregable (anulando las funciones virtuales).
Pero tienes razón, no tiene muchos ejemplos del mundo real.
Creo que la sección crítica de C ++ FAQ Lite es:
Un uso legítimo y a largo plazo para la herencia privada es cuando se quiere construir una clase Fred que usa código en una clase Wilma, y el código de la clase Wilma necesita invocar funciones miembro de su nueva clase, Fred. En este caso, Fred llama a los no virtuales en Wilma, y Wilma llama (generalmente virtuales puros) en sí mismo, que son reemplazados por Fred. Esto sería mucho más difícil de hacer con la composición.
Si tiene dudas, debería preferir la composición a la herencia privada.
El uso canónico de la herencia privada es la relación "implementada en términos de" (gracias a "Effective C ++" de Scott Meyers para esta redacción). En otras palabras, la interfaz externa de la clase que hereda no tiene ninguna relación (visible) con la clase heredada, pero la usa internamente para implementar su funcionalidad.
Encontré una buena aplicación para herencia privada, aunque tiene un uso limitado.
Problema para resolver
Supongamos que le dan la siguiente API C:
#ifdef __cplusplus
extern "C" {
#endif
typedef struct
{
/* raw owning pointer, it''s C after all */
char const * name;
/* more variables that need resources
* ...
*/
} Widget;
Widget const * loadWidget();
void freeWidget(Widget const * widget);
#ifdef __cplusplus
} // end of extern "C"
#endif
Ahora su trabajo es implementar esta API usando C ++.
Enfoque C-ish
Por supuesto, podríamos elegir un estilo de implementación C-ish de la siguiente manera:
Widget const * loadWidget()
{
auto result = std::make_unique<Widget>();
result->name = strdup("The Widget name");
// More similar assignments here
return result.release();
}
void freeWidget(Widget const * const widget)
{
free(result->name);
// More similar manual freeing of resources
delete widget;
}
Pero hay varias desventajas:
- Gestión manual de recursos (p. Ej. Memoria)
- Es fácil configurar la
struct
incorrecta - Es fácil olvidar liberar los recursos al liberar la
struct
- Es C-ish
Enfoque C ++
Podemos usar C ++, entonces ¿por qué no usar todos sus poderes?
Presentamos la administración automatizada de recursos
Los problemas anteriores están básicamente vinculados a la gestión de recursos manual. La solución que viene a la mente es heredar de Widget
y agregar una instancia de administración de recursos a la clase derivada WidgetImpl
para cada variable:
class WidgetImpl : public Widget
{
public:
// Added bonus, Widget''s members get default initialized
WidgetImpl()
: Widget()
{}
void setName(std::string newName)
{
m_nameResource = std::move(newName);
name = m_nameResource.c_str();
}
// More similar setters to follow
private:
std::string m_nameResource;
};
Esto simplifica la implementación a lo siguiente:
Widget const * loadWidget()
{
auto result = std::make_unique<WidgetImpl>();
result->setName("The Widget name");
// More similar setters here
return result.release();
}
void freeWidget(Widget const * const widget)
{
// No virtual destructor in the base class, thus static_cast must be used
delete static_cast<WidgetImpl const *>(widget);
}
De esta manera, remediamos todos los problemas anteriores. Pero un cliente todavía puede olvidarse de los setters de WidgetImpl
y asignarlos a los miembros de Widget
directamente.
Herencia privada entra al escenario
Para encapsular los miembros de Widget
usamos herencia privada. Lamentablemente, ahora necesitamos dos funciones adicionales para transmitir entre ambas clases:
class WidgetImpl : private Widget
{
public:
WidgetImpl()
: Widget()
{}
void setName(std::string newName)
{
m_nameResource = std::move(newName);
name = m_nameResource.c_str();
}
// More similar setters to follow
Widget const * toWidget() const
{
return static_cast<Widget const *>(this);
}
static void deleteWidget(Widget const * const widget)
{
delete static_cast<WidgetImpl const *>(widget);
}
private:
std::string m_nameResource;
};
Esto hace las siguientes adaptaciones necesarias:
Widget const * loadWidget()
{
auto widgetImpl = std::make_unique<WidgetImpl>();
widgetImpl->setName("The Widget name");
// More similar setters here
auto const result = widgetImpl->toWidget();
widgetImpl.release();
return result;
}
void freeWidget(Widget const * const widget)
{
WidgetImpl::deleteWidget(widget);
}
Esta solución resuelve todos los problemas. No hay ningún administrador de memoria manual y Widget
bien encapsulado para que WidgetImpl
ya no tenga ningún miembro público de datos. Hace que la implementación sea fácil de usar, correcta y difícil (¿imposible?) Para utilizarla de manera incorrecta.
Los fragmentos de código forman un ejemplo de compilación en Coliru .
Herencia privada que se utilizará cuando la relación no sea "es una", pero la nueva clase se puede "implementar en el término de la clase existente" o la nueva clase "funcionar como" clase existente.
ejemplo de "Estándares de codificación C ++ por Andrei Alexandrescu, Herb Sutter": - Tenga en cuenta que dos clases Cuadrado y Rectángulo tienen funciones virtuales para establecer su altura y ancho. Entonces Square no puede heredar correctamente de Rectangle, porque el código que usa un rectángulo modificable asumirá que SetWidth no cambia la altura (ya sea que Rectangle documente explícitamente ese contrato o no), mientras que Square :: SetWidth no puede conservar ese contrato y su propia cuadratura invariante en al mismo tiempo. Pero Rectangle tampoco puede heredar correctamente de Square, si los clientes de Square suponen, por ejemplo, que el área de un Square tiene su ancho al cuadrado o si dependen de alguna otra propiedad que no sea válida para Rectangles.
Un rectángulo cuadrado "is-a" (matemáticamente) pero un Square no es un rectángulo (conductualmente). En consecuencia, en lugar de "is-a", preferimos decir "works-like-a" (o, si lo prefiere, "usable-as-a") para que la descripción sea menos propensa a malentendidos.
Lo uso todo el tiempo. Algunos ejemplos fuera de mi cabeza:
- Cuando quiero exponer algunas, pero no todas, la interfaz de una clase base. La herencia pública sería una mentira, ya que la sustituibilidad de Liskov se rompe, mientras que la composición significaría escribir un montón de funciones de reenvío.
- Cuando quiero derivar de una clase concreta sin un destructor virtual. La herencia pública invitaría a los clientes a eliminar a través de un puntero a base, invocando un comportamiento indefinido.
Un ejemplo típico se deriva de forma privada de un contenedor STL:
class MyVector : private vector<int>
{
public:
// Using declarations expose the few functions my clients need
// without a load of forwarding functions.
using vector<int>::push_back;
// etc...
};
- Al implementar el Patrón de adaptador, heredar de forma privada de la clase Adaptado ahorra tener que reenviar a una instancia cerrada.
- Para implementar una interfaz privada. Esto aparece a menudo con el patrón de observador. Normalmente, mi clase Observer, dice MyClass, se suscribe a sí misma con algún Subject. Entonces, solo MyClass necesita hacer la clase MyClass -> conversión del observador. El resto del sistema no necesita saber al respecto, por lo que se indica la herencia privada.
Me resulta útil para las interfaces (es decir, las clases abstractas) que estoy heredando, donde no quiero que otro código toque la interfaz (solo la clase que hereda).
[editado en un ejemplo]
Tome el example vinculado a arriba. Diciendo que
[...] clase Wilma necesita invocar funciones miembro de su nueva clase, Fred.
es decir que Wilma le está pidiendo a Fred que pueda invocar ciertas funciones miembro, o más bien, está diciendo que Wilma es una interfaz . Por lo tanto, como se menciona en el ejemplo
la herencia privada no es malvada; es más costoso de mantener, ya que aumenta la probabilidad de que alguien cambie algo que pueda romper tu código.
comentarios sobre el efecto deseado de los programadores que necesitan cumplir con nuestros requisitos de interfaz, o romper el código. Y, como fredCallsWilma () está protegido, solo los amigos y las clases derivadas pueden tocarlo, es decir, una interfaz heredada (clase abstracta) que solo la clase que hereda puede tocar (y amigos).
[editado en otro ejemplo]
Esta página trata brevemente las interfaces privadas (desde otro ángulo).
Nota después de la aceptación de la respuesta: esta NO es una respuesta completa. Lea otras respuestas como here (conceptualmente) y here (tanto teóricas como prácticas) si le interesa la pregunta. Este es solo un truco elegante que se puede lograr con herencia privada. Si bien es elegante, no es la respuesta a la pregunta.
Además del uso básico de la herencia privada que se muestra en C ++ FAQ (vinculado en los comentarios de otros) puede usar una combinación de herencia privada y virtual para sellar una clase (en terminología .NET) o para hacer una clase final (en terminología Java) . Este no es un uso común, pero de todos modos lo encontré interesante:
class ClassSealer {
private:
friend class Sealed;
ClassSealer() {}
};
class Sealed : private virtual ClassSealer
{
// ...
};
class FailsToDerive : public Sealed
{
// Cannot be instantiated
};
Sellado puede ser instanciado. Se deriva de ClassSealer y puede llamar al constructor privado directamente ya que es un amigo.
FailsToDerive no se compilará, ya que debe llamar directamente al constructor ClassSealer (requisito de herencia virtual), pero no puede hacerlo ya que es privado en la clase Sealed y, en este caso, FailsToDerive no es amigo de ClassSealer .
EDITAR
Se mencionó en los comentarios que esto no podía hacerse genérico en ese momento utilizando CRTP. El estándar C ++ 11 elimina esa limitación al proporcionar una sintaxis diferente para los argumentos de la plantilla befriend:
template <typename T>
class Seal {
friend T; // not: friend class T!!!
Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...
Por supuesto, esto es discutible, ya que C ++ 11 proporciona una palabra clave contextual final
para exactamente este propósito:
class Sealed final // ...
Si la clase derivada necesita volver a usar el código y - no puede cambiar la clase base y - está protegiendo sus métodos usando los miembros de la base bajo un bloqueo.
entonces deberías usar herencia privada, de lo contrario, tienes el peligro de que los métodos básicos desbloqueados se exporten a través de esta clase derivada.
Solo porque C ++ tenga una función, no significa que sea útil o que deba usarse.
Diría que no deberías usarlo en absoluto.
Si lo estás usando de todos modos, bueno, básicamente estás violando la encapsulación y bajando la cohesión. Está colocando datos en una clase y agregando métodos que manipulan los datos en otra.
Al igual que otras funciones de C ++, se puede usar para lograr efectos secundarios, como sellar una clase (como se menciona en la respuesta de dribeas), pero esto no la convierte en una buena característica.
Un uso útil de la herencia privada es cuando tienes una clase que implementa una interfaz, que luego se registra con algún otro objeto. Haces que la interfaz sea privada para que la clase misma tenga que registrarse y solo el objeto específico con el que está registrado pueda usar esas funciones.
Por ejemplo:
class FooInterface
{
public:
virtual void DoSomething() = 0;
};
class FooUser
{
public:
bool RegisterFooInterface(FooInterface* aInterface);
};
class FooImplementer : private FooInterface
{
public:
explicit FooImplementer(FooUser& aUser)
{
aUser.RegisterFooInterface(this);
}
private:
virtual void DoSomething() { ... }
};
Por lo tanto, la clase FooUser puede llamar a los métodos privados de FooImplementer a través de la interfaz FooInterface, mientras que otras clases externas no pueden. Este es un gran patrón para manejar devoluciones de llamada específicas que se definen como interfaces.
Una clase contiene un invariante. El invariante es establecido por el constructor. Sin embargo, en muchas situaciones es útil tener una vista del estado de representación del objeto (que puede transmitir a través de la red o guardar en un archivo, DTO si lo prefiere). REST se realiza mejor en términos de un Tipo Agregado. Esto es especialmente cierto si eres const correcto. Considerar:
struct QuadraticEquationState {
const double a;
const double b;
const double c;
// named ctors so aggregate construction is available,
// which is the default usage pattern
// add your favourite ctors - throwing, try, cps
static QuadraticEquationState read(std::istream& is);
static std::optional<QuadraticEquationState> try_read(std::istream& is);
template<typename Then, typename Else>
static std::common_type<
decltype(std::declval<Then>()(std::declval<QuadraticEquationState>()),
decltype(std::declval<Else>()())>::type // this is just then(qes) or els(qes)
if_read(std::istream& is, Then then, Else els);
};
// this works with QuadraticEquation as well by default
std::ostream& operator<<(std::ostream& os, const QuadraticEquationState& qes);
// no operator>> as we''re const correct.
// we _might_ (not necessarily want) operator>> for optional<qes>
std::istream& operator>>(std::istream& is, std::optional<QuadraticEquationState>);
struct QuadraticEquationCache {
mutable std::optional<double> determinant_cache;
mutable std::optional<double> x1_cache;
mutable std::optional<double> x2_cache;
mutable std::optional<double> sum_of_x12_cache;
};
class QuadraticEquation : public QuadraticEquationState, // private if base is non-const
private QuadraticEquationCache {
public:
QuadraticEquation(QuadraticEquationState); // in general, might throw
QuadraticEquation(const double a, const double b, const double c);
QuadraticEquation(const std::string& str);
QuadraticEquation(const ExpressionTree& str); // might throw
}
En este punto, puede almacenar colecciones de caché en contenedores y buscarlas en la construcción. Útil si hay un procesamiento real. Tenga en cuenta que la memoria caché es parte de la QE: las operaciones definidas en la QE pueden significar que la memoria caché es parcialmente reutilizable (por ejemplo, c no afecta la suma); sin embargo, cuando no hay caché, vale la pena buscarlo.
La herencia privada casi siempre puede ser modelada por un miembro (almacenando referencias a la base si es necesario). Simplemente no siempre vale la pena modelar de esa manera; a veces la herencia es la representación más eficiente.