programacion - herencia virtual c++
¿Cuándo la herencia virtual ES un buen diseño? (7)
EDIT3: asegúrese de entender claramente lo que pregunto antes de responder (hay EDIT2 y muchos comentarios). Hay (o hubo) muchas respuestas que muestran claramente una falta de comprensión de la pregunta (sé que también es mi culpa, lo siento por eso)
Hola, he revisado las preguntas sobre herencia virtual ( class B: public virtual A {...}
) en C ++, pero no encontré una respuesta a mi pregunta.
Sé que hay algunos problemas con la herencia virtual, pero lo que me gustaría saber es en qué casos la herencia virtual se consideraría un buen diseño.
Vi personas que mencionan interfaces como IUnknown
o ISerializable
, y también que el diseño de iostream
se basa en la herencia virtual. ¿Serían esos buenos ejemplos de un buen uso de la herencia virtual, es solo porque no hay una mejor alternativa o porque la herencia virtual es el diseño adecuado en este caso? Gracias.
EDITAR: Para aclarar, estoy preguntando acerca de ejemplos de la vida real, por favor, no dé ejemplos abstractos. Sé qué es la herencia virtual y qué patrón de herencia lo requiere, lo que quiero saber es cuándo es la buena manera de hacer las cosas y no solo una consecuencia de la herencia compleja.
EDIT2: En otras palabras, quiero saber cuándo la jerarquía de diamantes (que es la razón de la herencia virtual) es un buen diseño
Como está solicitando ejemplos específicos, sugeriré un recuento de referencias intrusivo. No es que la herencia virtual sea un buen diseño en este caso, pero la herencia virtual es la herramienta correcta para que el trabajo funcione correctamente.
A principios de los años 90, utilicé una biblioteca de clases que tenía una clase ReferenceCounted
que derivaría de otras clases para darle un recuento de referencias y un par de métodos para administrar el recuento de referencias. La herencia tenía que ser virtual, de lo contrario, si tuviera varias bases que cada una derivara no virtualmente de ReferenceCounted
, terminaría con múltiples recuentos de referencias. La herencia virtual aseguró que tendría un único recuento de referencia para sus objetos.
El recuento de referencias no intrusivas con shared_ptr
y otros parece ser más popular en estos días, pero el recuento de referencias intrusivo sigue siendo útil cuando una clase pasa this
a otros métodos. Los recuentos de referencias externas se pierden en este caso. También me gusta que el recuento de referencias intrusivas dice sobre una clase cómo se gestiona el ciclo de vida de los objetos de esa clase.
[Creo que estoy defendiendo el recuento de referencias intrusivas porque lo veo muy raramente en estos días, pero me gusta.]
Grrr .. La herencia virtual DEBE utilizarse para el subtipo de abstracción. No hay ninguna opción si desea obedecer los principios de diseño de OO. De lo contrario, evita que otros programadores deriven otros subtipos.
Un ejemplo abstracto primero: tiene una abstracción básica A. Desea crear un subtipo B. Tenga en cuenta que subtipo significa necesariamente otra abstracción. Si no es abstracto, es una implementación no un tipo.
Ahora viene otro programador y quiere hacer un subtipo C de A. Genial.
Finalmente, otro programador viene y quiere algo que es tanto una B como una C. También es una A, por supuesto. En estos escenarios la herencia virtual es obligatoria .
Aquí hay un ejemplo del mundo real: desde un compilador, modelando tipos de datos:
struct function { ..
struct int_to_float_type : virtual function { ..
struct cloneable : virtual function { ..
struct cloneable_int_to_float_type :
virtual function,
virtual int_to_float_type
virtual cloneable
{ ..
struct function_f : cloneable_int_to_float_type {
Aquí, la function
representa funciones, int_to_float_type
representa un subtipo que consiste en funciones de int a float. Cloneable
es una propiedad especial que la función puede ser clonada. function_f
es una function_f
concreta (no abstracta).
Tenga en cuenta que si no hice originalmente la function
una base virtual de int_to_float_type
no podría cloneable
(y viceversa).
En general, si sigue un estilo OOP "estricto", siempre definirá una red de abstracciones, y luego se derivarán implementaciones para ellas. Se separan estrictamente los subtipos que solo se aplican a las abstracciones y la implementación .
En Java, esto es obligatorio (las interfaces no son clases). En C ++ no se aplica, y no tiene que seguir el patrón, pero debe ser consciente de ello, y cuanto más grande sea el equipo con el que está trabajando o el proyecto en el que está trabajando, más fuerte será la razón. Tendrás que apartarte de ello.
La escritura mixta requiere una gran cantidad de tareas domésticas en C ++. En Ocaml, las clases y los tipos de clases son independientes y están emparejados por la estructura (posesión de métodos o no), por lo que la herencia es siempre una comodidad. En realidad, esto es mucho más fácil de usar que la tipificación nominal. Los mixins proporcionan una manera de simular la tipificación estructural en un lenguaje que solo tiene tipificación nominal.
La herencia virtual es necesaria cuando se ve obligado a usar la herencia múltiple. Hay algunos problemas que no se pueden resolver de forma limpia / fácil al evitar la herencia múltiple. En esos casos (que son raros), deberá considerar la herencia virtual. El 95% de las veces, puede (y debe) evitar la herencia múltiple para salvarse (y a aquellos que buscan su código después de usted) muchos dolores de cabeza.
Como nota al margen, COM no te obliga a usar herencia múltiple. Es posible (y bastante común) crear un objeto COM derivado de IUnknown (directa o indirectamente) que tenga un árbol de herencia lineal.
La herencia virtual es una buena opción de diseño para el caso cuando una clase A se extiende a otra clase B, pero B no tiene funciones de miembro virtual que no sean posiblemente el destructor. Puede pensar en clases como B como mixins , donde una jerarquía de tipos necesita solo una clase base del tipo mixin para poder beneficiarse de ella.
Un buen ejemplo es la herencia virtual que se utiliza con algunas de las plantillas de iostream en la implementación libstdc ++ de la STL. Por ejemplo, libstdc ++ declara la plantilla basic_istream
con:
template<typename _CharT, typename _Traits>
class basic_istream : virtual public basic_ios<_CharT, _Traits>
Utiliza la herencia virtual para extender basic_ios<_CharT, _Traits>
porque istreams solo debería tener una entrada streambuf, y muchas operaciones de un istream siempre deberían tener la misma funcionalidad (especialmente la función miembro rdbuf
para obtener la única entrada streambuf).
Ahora imagine que escribe una clase ( baz_reader
) que extiende std::istream
con una función miembro para leer en objetos de tipo baz
, y otra clase ( bat_reader
) que extiende std::istream
con una función miembro para leer en objetos de tipo bat
Puede tener una clase que amplíe baz_reader
y bat_reader
. Si no se utilizara la herencia virtual, las bases baz_reader
y bat_reader
tendrían su propia entrada streambuf, probablemente no la intención. Probablemente querrá que las bases baz_reader
y bat_reader
lean del mismo streambuf. Sin la herencia virtual en std::istream
para extender std::basic_ios<char>
, puede lograrlo configurando los readbufs miembros de las bases baz_reader
y bat_reader
en el mismo objeto streambuf, pero luego tendría dos copias del puntero para El streambuf cuando uno bastaría.
La herencia virtual no es una cosa buena o mala, es un detalle de implementación, como cualquier otro, y existe para implementar código donde ocurren las mismas abstracciones. Esto suele ser lo correcto cuando el código debe ser súper tiempo de ejecución, por ejemplo, en COM donde algunos objetos COM deben compartirse entre procesos, y mucho menos compiladores, lo que requiere el uso de IUnknown donde las bibliotecas normales de C ++ simplemente usarían shared_ptr
. Como tal, en mi opinión, el código normal de C ++ debería depender de plantillas y similares y no debería requerir herencia virtual, pero es totalmente necesario en algunos casos especiales.
Si tiene una jerarquía de interfaz y una jerarquía de implementación correspondiente, es necesario realizar las bases virtuales de las clases base de la interfaz.
P.ej
struct IBasicInterface
{
virtual ~IBasicInterface() {}
virtual void f() = 0;
};
struct IExtendedInterface : virtual IBasicInterface
{
virtual ~IExtendedInterface() {}
virtual void g() = 0;
};
// One possible implementation strategy
struct CBasicImpl : virtual IBasicInterface
{
virtual ~CBasicImpl() {}
virtual void f();
};
struct CExtendedImpl : virtual IExtendedInterface, CBasicImpl
{
virtual ~CExtendedImpl() {}
virtual void g();
};
Por lo general, esto solo tiene sentido si tiene varias interfaces que amplían la interfaz básica y se requiere más de una estrategia de implementación en diferentes situaciones. De esta manera, tiene una jerarquía de interfaz clara y sus jerarquías de implementación pueden usar la herencia para evitar la duplicación de implementaciones comunes. Sin embargo, si estás utilizando Visual Studio, obtienes mucha advertencia C4250.
Para evitar el corte accidental, generalmente es mejor que las clases CBasicImpl
y CExtendedImpl
no sean instanciables, sino que tengan un nivel adicional de herencia que no proporcione ninguna funcionalidad adicional, excepto un constructor.
Estas preguntas frecuentes respondieron todas las preguntas posibles relacionadas con la herencia virtual. Incluso la respuesta a su pregunta (si reconocí sus preguntas correctamente;)): Pregunta 25.5.