A diferencia de la herencia protegida, la herencia privada de C ++ se abrió camino en el desarrollo general de C ++. Sin embargo, todavía no le he encontrado un buen uso.
¿Cuándo lo usan ustedes?
Nota después de la aceptación de la respuesta: Esta NO es una respuesta completa. Lea otras respuestas como aquí (conceptualmente) y aquí (tanto teóricas como prácticas) si está interesado en la pregunta. Este es solo un truco elegante que se puede lograr con la 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 las Preguntas frecuentes de C ++ (enlazadas 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 que una clase sea 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
};
Sealed se puede crear una instancia. Se deriva de ClassSealer y puede llamar al constructor privado directamente ya que es un amigo.
FailsToDerive no compilará ya que debe llamar directamente al constructor ClassSealer (requisito de herencia virtual), pero no puede 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 usando CRTP. El estándar C ++ 11 elimina esa limitación al proporcionar una sintaxis diferente para hacerse amigo de los argumentos de la plantilla:
template <typename T>
class Seal {
friend T; // not: friend class T!!!
Seal() {}
};
class Sealed : private virtual Seal<Sealed> // ...
Por supuesto, todo esto es discutible, ya que C ++ 11 proporciona una final
palabra clave contextual exactamente para este propósito:
class Sealed final // ...
Lo uso todo el tiempo. Algunos ejemplos fuera de mi cabeza:
Un ejemplo típico es derivar 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...
};
push_back
, las MyVector
obtiene gratis.
template<typename... Args> constexpr decltype(auto) f(Args && ... args) noexcept(noexcept(std::declval<Base &>().f(std::forward<Args>(args)...)) and std::is_nothrow_move_constructible<decltype(std::declval<Base &>().f(std::forward<Args>(args)...))>) { return m_base.f(std::forward<Args>(args)...); }
o podrías escribir usando Base::f;
. Si desea que la mayor parte de la funcionalidad y flexibilidad que la herencia privada y una using
declaración que usted da, usted tiene ese monstruo para cada función (y no se olvide de const
y volatile
sobrecargas!).
El uso canónico de la herencia privada es la relación "implementada en términos de" (gracias a 'Effective C ++' de Scott Meyers por esta redacción). En otras palabras, la interfaz externa de la clase heredada no tiene relación (visible) con la clase heredada, pero la usa internamente para implementar su funcionalidad.
Un uso útil de la herencia privada es cuando tiene una clase que implementa una interfaz, que luego se registra con algún otro objeto. Usted hace que esa 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.
Creo que la sección crítica de C ++ FAQ Lite es:
Un uso legítimo a largo plazo para la herencia privada es cuando desea 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 a los virtuales puros) en sí mismo, que son anulados por Fred. Esto sería mucho más difícil de hacer con la composición.
En caso de duda, debe preferir la composición a la herencia privada.
Lo encuentro útil para interfaces (es decir, clases abstractas) que estoy heredando donde no quiero que otro código toque la interfaz (solo la clase heredada).
[editado en un ejemplo]
Tome el ejemplo vinculado anteriormente. Diciendo que
[...] clase Wilma necesita invocar funciones miembro de su nueva clase, Fred.
es decir que Wilma está requiriendo que Fred 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 mala; es más caro de mantener, ya que aumenta la probabilidad de que alguien cambie algo que rompa su código.
comenta sobre el efecto deseado de los programadores que necesitan cumplir con nuestros requisitos de interfaz o descifrar el código. Y, dado que fredCallsWilma () está protegido, solo los amigos y las clases derivadas pueden tocarlo, es decir, una interfaz heredada (clase abstracta) que solo la clase heredada puede tocar (y amigos).
[editado en otro ejemplo]
Esta página analiza brevemente las interfaces privadas (desde otro ángulo).
A veces encuentro ú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 que expone, 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 hacerlo static_cast<BigClass *>(this)
. No es necesario que un miembro de datos adicional ocupe espacio.
BigClass
¿hay en este ejemplo? Encuentro esto interesante, pero grita hack en mi cara.
Encontré una buena aplicación para la herencia privada, aunque tiene un uso limitado.
Suponga que recibe 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 ++.
Por supuesto, podríamos elegir un estilo de implementación C-ish así:
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:
struct
malstruct
Se nos permite usar C ++, entonces, ¿por qué no usar todos sus poderes?
Básicamente, todos los problemas anteriores están relacionados con la gestión manual de recursos. La solución que me viene a la mente es heredar 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);
}
Así solucionamos todos los problemas anteriores. Pero un cliente aún puede olvidarse de los establecedores WidgetImpl
y asignar a los Widget
miembros directamente.
Para encapsular los Widget
miembros usamos herencia privada. Lamentablemente, ahora necesitamos dos funciones adicionales para lanzar 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 necesarias las siguientes adaptaciones:
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. Sin gestión de memoria manual y Widget
está muy bien encapsulado para que WidgetImpl
ya no tenga miembros de datos públicos. Hace que la implementación sea fácil de usar correctamente y difícil (¿imposible?) De usar incorrectamente.
Los fragmentos de código forman un ejemplo de compilación en Coliru .
Si es una clase derivada, necesita reutilizar el código y no puede cambiar la clase base y está protegiendo sus métodos usando los miembros de la base bajo un candado.
entonces debería usar la herencia privada, de lo contrario corre el peligro de que los métodos base desbloqueados se exporten a través de esta clase derivada.
A veces, podría ser una alternativa a la agregación , por ejemplo, si desea la agregación pero con un comportamiento modificado de la entidad agregable (anulando las funciones virtuales).
Pero tienes razón, no tiene muchos ejemplos del mundo real.
Herencia privada que se utilizará cuando la relación no sea "es un", pero la nueva clase se puede "implementar en términos de la clase existente" o la nueva clase "funciona como" la clase existente.
ejemplo de "Estándares de codificación C ++ de Andrei Alexandrescu, Herb Sutter": - Considere 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 Rectangle 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 preservar 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 es su ancho al cuadrado, o si confían en alguna otra propiedad que no es válida para Rectangles.
Un cuadrado "es un" rectángulo (matemáticamente), pero un cuadrado no es un rectángulo (en términos de comportamiento). En consecuencia, en lugar de "es-a", preferimos decir "funciona-como-a" (o, si lo prefiere, "utilizable-como-a") para que la descripción sea menos propensa a malentendidos.
Una clase tiene una invariante. El invariante lo establece 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 AggregateType. Esto es especialmente cierto si estás en lo 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 algún procesamiento real. Tenga en cuenta que el caché es parte del QE: las operaciones definidas en el QE pueden significar que el 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 modelarla un miembro (almacenando la referencia 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 eficaz.
Si necesita una std::ostream
con algunos pequeños cambios (como en esta pregunta ), es posible que deba
MyStreambuf
que derive std::streambuf
e implemente cambios allí.MyOStream
que se derive de std::ostream
eso también inicializa y administra una instancia de MyStreambuf
y pasa el puntero a esa instancia al constructor destd::ostream
La primera idea podría ser agregar la MyStream
instancia como miembro de datos a la MyOStream
clase:
class MyOStream : public std::ostream
{
public:
MyOStream()
: std::basic_ostream{ &m_buf }
, m_buf{}
{}
private:
MyStreambuf m_buf;
};
Pero las clases base se construyen antes que cualquier miembro de datos, por lo que se pasa un puntero a una std::streambuf
instancia aún no construida a la std::ostream
que se le asigna un comportamiento indefinido.
La solución se propone en la respuesta de Ben a la pregunta antes mencionada , simplemente herede primero del búfer de flujo, luego del flujo y luego inicialice el flujo con this
:
class MyOStream : public MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
Sin embargo, la clase resultante también podría usarse como una std::streambuf
instancia que generalmente no es deseada. Cambiar a herencia privada resuelve este problema:
class MyOStream : private MyStreamBuf, public std::ostream
{
public:
MyOStream()
: MyStreamBuf{}
, basic_ostream{ this }
{}
};
El hecho de que C ++ tenga una característica no significa que sea útil o que deba usarse.
Yo diría que no deberías usarlo en absoluto.
Si lo está usando de todos modos, bueno, básicamente está violando la encapsulación y reduciendo la cohesión. Estás poniendo datos en una clase y agregando métodos que manipulan los datos en otra.
Al igual que otras características 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.