¿Cuándo debo usar la herencia privada de C ++?

116

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?


fuente

Respuestas:

60

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 finalpalabra clave contextual exactamente para este propósito:

class Sealed final // ...
David Rodríguez - dribeas
fuente
Esa es una gran técnica. Escribiré una entrada de blog sobre él.
1
Pregunta: si no usáramos herencia virtual, entonces FailsToDerive compilaría. ¿Correcto?
4
+1. @Sasha: Correcto, se necesita herencia virtual ya que la clase más derivada siempre llama directamente a los constructores de todas las clases heredadas virtualmente, lo que no es el caso de la herencia simple.
j_random_hacker
5
¡Esto puede hacerse genérico, sin hacer un ClassSealer personalizado para cada clase que desee sellar! Compruébelo usted mismo: class ClassSealer {protected: ClassSealer () {}}; eso es todo.
+1 Iraimbilanja, ¡genial! Por cierto, vi su comentario anterior (ahora eliminado) sobre el uso de CRTP: creo que, de hecho, debería funcionar, es complicado obtener la sintaxis correcta para los amigos de plantilla. Pero en cualquier caso, su solución sin plantilla es mucho más impresionante :)
j_random_hacker
138

Lo uso todo el tiempo. Algunos ejemplos fuera de mi cabeza:

  • Cuando quiero exponer parte de la interfaz de una clase base, pero no toda. La herencia pública sería una mentira, ya que la sustituibilidad de Liskov está rota, 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 la base, invocando un comportamiento indefinido.

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...  
};
  • Al implementar el patrón del adaptador, heredar de forma privada de la clase adaptada evita tener que reenviar a una instancia adjunta.
  • Implementar una interfaz privada. Esto surge a menudo con el patrón de observador. Por lo general, mi clase de observador, dice MyClass, se suscribe con algún tema. Entonces, solo MyClass necesita hacer la conversión MyClass -> Observer. El resto del sistema no necesita saberlo, por lo que se indica la herencia privada.
fizzer
fuente
4
@Krsna: En realidad, no lo creo. Aquí solo hay una razón: la pereza, aparte de la última, que sería más difícil de evitar.
Matthieu M.
11
No tanta pereza (a menos que lo diga en serio). Esto permite la creación de nuevas sobrecargas de funciones que se han expuesto sin ningún trabajo adicional. Si en C ++ 1x añaden 3 nuevas sobrecargas push_back, las MyVectorobtiene gratis.
David Stone
@DavidStone, ¿no puedes hacerlo con un método de plantilla?
Julien__
5
@Julien__: Sí, podrías escribir 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 usingdeclaración que usted da, usted tiene ese monstruo para cada función (y no se olvide de consty volatilesobrecargas!).
David Stone
2
Digo la mayor parte de la funcionalidad porque todavía está invocando un constructor de movimiento adicional que no está presente en la versión de la instrucción using. En general, esperaría que esto se optimizara, pero la función teóricamente podría devolver un tipo no móvil por valor. La plantilla de la función de reenvío también tiene una instancia de plantilla adicional y profundidad de constexpr. Esto puede hacer que su programa se encuentre con límites de implementación.
David Stone
31

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.

Harper Shelby
fuente
6
Vale la pena mencionar una de las razones por las que se usa en este caso: Esto permite que se realice la optimización de la clase base vacía, lo que no ocurrirá si la clase hubiera sido miembro en lugar de una clase base.
jalf
2
su uso principal es reducir el consumo de espacio donde realmente importa, por ejemplo, en clases de cadenas controladas por políticas o en pares comprimidos. en realidad, boost :: compressed_pair usó herencia protegida.
Johannes Schaub - litb
jalf: Oye, no me di cuenta de eso. Pensé que la herencia no pública se usaba principalmente como un truco cuando necesitas acceder a los miembros protegidos de una clase. Sin embargo, me pregunto por qué un objeto vacío ocuparía espacio cuando se usa la composición. Probablemente para direccionabilidad universal ...
3
También es útil hacer que una clase no se pueda copiar, simplemente heredar de forma privada de una clase vacía que no se puede copiar. Ahora no tiene que pasar por el ajetreado trabajo de declarar pero no definir un constructor de copia privada y un operador de asignación. Meyers también habla de esto.
Michael Burr
No me di cuenta de que esta pregunta en realidad se trata de herencia privada en lugar de herencia protegida. sí, supongo que hay bastantes aplicaciones para ello. Sin embargo, no puedo pensar en muchos ejemplos de herencia protegida: / parece que eso solo rara vez es útil.
Johannes Schaub - litb
23

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.

Daemin
fuente
1
De hecho, la herencia privada es IS-A privada.
Curioso
18

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.

Bill el lagarto
fuente
4

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).

parcialidad
fuente
Realmente no suena útil ... ¿puedes publicar un ejemplo?
Creo que veo a dónde vas ... Un caso de uso típico podría ser que Wilma es algún tipo de clase de utilidad que necesita llamar a funciones virtuales en Fred, pero otras clases no necesitan saber que Fred está implementado en términos. de Wilma. ¿Correcto?
j_random_hacker
Si. Debo señalar que, a mi entender, el término "interfaz" se usa más comúnmente en Java. Cuando lo escuché por primera vez, pensé que se le podría haber dado un nombre mejor. Ya que, en este ejemplo, tenemos una interfaz con la que nadie interactúa de la forma en que normalmente pensamos en la palabra.
sesgo
@Noos: Sí, creo que su afirmación "Wilma es una interfaz" es un poco ambigua, ya que la mayoría de la gente entendería que Wilma es una interfaz que Fred pretende proporcionar al mundo , en lugar de un contrato con Wilma únicamente.
j_random_hacker
@j_ Por eso creo que la interfaz es un mal nombre. Interfaz, el término, no tiene por qué significar para el mundo como uno pensaría, sino que es una garantía de funcionalidad. En realidad, tenía dudas sobre el término interfaz en mi clase de Diseño de programas. Pero, usamos lo que se nos da ...
sesgo
2

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.


fuente
No hay necesidad de la declaración de avance de BigClass¿hay en este ejemplo? Encuentro esto interesante, pero grita hack en mi cara.
Thomas Eding
2

Encontré una buena aplicación para la herencia privada, aunque tiene un uso limitado.

Problema a resolver

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 ++.

Enfoque C-ish

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:

  • Gestión manual de recursos (por ejemplo, memoria)
  • Es fácil configurar el structmal
  • Es fácil olvidarse de liberar los recursos al liberar el struct
  • Es C-ish

Enfoque C ++

Se nos permite usar C ++, entonces, ¿por qué no usar todos sus poderes?

Presentamos la gestión automatizada de recursos

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 Widgety agregar una instancia de administración de recursos a la clase derivada WidgetImplpara 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 WidgetImply asignar a los Widgetmiembros directamente.

La herencia privada entra en escena

Para encapsular los Widgetmiembros 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 Widgetestá muy bien encapsulado para que WidgetImplya 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 .

Matthäus Brandl
fuente
1

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.

sportswithin.com
fuente
1

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.

Bayda
fuente
0

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.

Rahul_cs12
fuente
0

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.

lorro
fuente
0

Si necesita una std::ostreamcon algunos pequeños cambios (como en esta pregunta ), es posible que deba

  1. Crear una clase MyStreambufque derive std::streambufe implemente cambios allí.
  2. Cree una clase MyOStreamque se derive de std::ostreameso también inicializa y administra una instancia de MyStreambufy pasa el puntero a esa instancia al constructor destd::ostream

La primera idea podría ser agregar la MyStreaminstancia como miembro de datos a la MyOStreamclase:

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::streambufinstancia aún no construida a la std::ostreamque 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::streambufinstancia 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 }
    {}
};
Matthäus Brandl
fuente
-1

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.

hasen
fuente
¿estas siendo sarcastico? ¡todo lo que tengo es un -1! de todos modos no eliminaré esto incluso si obtiene -100 votos
hasen
9
" básicamente está violando la encapsulación " ¿Puede dar un ejemplo?
Curioso
1
los datos en una clase y el comportamiento en otra suena como un aumento en la flexibilidad, ya que puede haber más de una clase de comportamiento y los clientes eligen cuál necesitan para satisfacer lo que quieren
makar