¿Por qué debería evitar la herencia múltiple en C ++?

Respuestas:

259

La herencia múltiple (abreviada como MI) huele , lo que significa que generalmente mal , por lo , se hizo por malas razones, y volverá a golpear al mantenedor.

Resumen

  1. Considere la composición de características, en lugar de la herencia.
  2. Ten cuidado con el diamante del terror
  3. Considere la herencia de múltiples interfaces en lugar de objetos
  4. A veces, la herencia múltiple es lo correcto. Si es así, úsalo.
  5. Prepárese para defender su arquitectura heredada múltiple en revisiones de código

1. ¿Quizás composición?

Esto es cierto para la herencia y, por lo tanto, es aún más cierto para la herencia múltiple.

¿Su objeto realmente necesita heredar de otro? A Carno necesita heredar de a Enginepara trabajar, ni de a Wheel. A Cartiene un Enginey cuatro Wheel.

Si usa la herencia múltiple para resolver estos problemas en lugar de la composición, entonces ha hecho algo mal.

2. El diamante del terror

Por lo general, tiene una clase A, By Cambas heredan de A. Y (no me pregunten por qué) a alguien, entonces decide que Ddebe heredar tanto desde ByC .

Me he encontrado con este tipo de problema dos veces en 8 años, y es divertido verlo por:

  1. Cuánto error fue desde el principio (en ambos casos, Dno debería haber heredado de ambos By C), porque esta era una mala arquitectura (de hecho, Cno debería haber existido en absoluto ...)
  2. La cantidad que los mantenedores pagaban por eso, porque en C ++, la clase principal Aestaba presente dos veces en su clase de nietos D, y por lo tanto, actualizar un campo principal A::fieldsignificaba actualizarlo dos veces (a través de B::fieldy C::field), o hacer que algo salga silenciosamente mal y se bloquee, más tarde (Nuevo puntero B::fieldy eliminar C::field...)

El uso de la palabra clave virtual en C ++ para calificar la herencia evita el diseño doble descrito anteriormente si esto no es lo que desea, pero de todos modos, en mi experiencia, probablemente esté haciendo algo mal ...

En la jerarquía de objetos, debe intentar mantener la jerarquía como un árbol (un nodo tiene UN padre), no como un gráfico.

Más sobre el diamante (editar 2017-05-03)

El verdadero problema con Diamond of Dread en C ++ ( suponiendo que el diseño sea sólido, ¡revise su código! ), Es que debe elegir :

  • ¿Es deseable que la clase Aexista dos veces en su diseño, y qué significa? Si es así, entonces herede de él dos veces.
  • si debería existir solo una vez, entonces herede de él virtualmente.

Esta elección es inherente al problema, y ​​en C ++, a diferencia de otros lenguajes, puede hacerlo sin dogmas que obliguen a su diseño a nivel de lenguaje.

Pero como todos los poderes, con ese poder viene la responsabilidad: hacer revisar su diseño.

3. Interfaces

La herencia múltiple de cero o una clase concreta, y cero o más interfaces generalmente está bien, porque no encontrará el Diamante del terror descrito anteriormente. De hecho, así es como se hacen las cosas en Java.

Por lo general, lo que quiere decir cuando C hereda de Ay Bes que los usuarios pueden usar Ccomo si fuera un Ay / o como si fuera un B.

En C ++, una interfaz es una clase abstracta que tiene:

  1. todo su método declarado puro virtual (sufijado por = 0) (eliminado el 2017-05-03)
  2. sin variables miembro

La herencia múltiple de cero a un objeto real, y cero o más interfaces no se considera "maloliente" (al menos, no tanto).

Más información sobre la interfaz abstracta de C ++ (editar 2017-05-03)

Primero, el patrón NVI se puede usar para producir una interfaz, porque el criterio real es no tener estado (es decir, no hay variables miembro, excepto this). El objetivo de su interfaz abstracta es publicar un contrato ("puede llamarme de esta manera y de esta manera"), nada más y nada menos. La limitación de tener solo un método virtual abstracto debe ser una elección de diseño, no una obligación.

En segundo lugar, en C ++, tiene sentido heredar virtualmente de interfaces abstractas (incluso con el costo adicional / indirección). Si no lo hace, y la herencia de la interfaz aparece varias veces en su jerarquía, entonces tendrá ambigüedades.

En tercer lugar, la orientación a objetos es excelente, pero no es la única verdad en TM en C ++. Use las herramientas adecuadas y recuerde siempre que tiene otros paradigmas en C ++ que ofrecen diferentes tipos de soluciones.

4. ¿Realmente necesitas herencia múltiple?

A veces sí.

Por lo general, su Cclase hereda de Ay B, y A, y Bson dos objetos no relacionados (es decir, no en la misma jerarquía, nada en común, diferentes conceptos, etc.).

Por ejemplo, podría tener un sistema Nodescon coordenadas X, Y, Z, capaz de hacer muchos cálculos geométricos (quizás un punto, parte de objetos geométricos) y cada Nodo es un Agente Automatizado, capaz de comunicarse con otros agentes.

Quizás ya tenga acceso a dos bibliotecas, cada una con su propio espacio de nombres (otra razón para usar espacios de nombres ... Pero usted usa espacios de nombres, ¿no?), Un ser geoy el otro serai

Entonces tienes tu propio own::Nodederivado de ai::Agenty geo::Point.

Este es el momento en que debes preguntarte si no deberías usar composición en su lugar. Si own::Noderealmente es tanto a ai::Agentcomo a geo::Point, entonces la composición no servirá.

Entonces necesitará herencia múltiple, own::Nodecomunicándose con otros agentes de acuerdo con su posición en un espacio 3D.

(Notará eso ai::Agenty estará geo::Pointcompletamente, totalmente, SIN RELACIÓN ... Esto reduce drásticamente el peligro de herencia múltiple)

Otros casos (editar 2017-05-03)

Hay otros casos:

  • usando la herencia (con suerte privada) como detalle de implementación
  • algunas expresiones idiomáticas de C ++, como las políticas, podrían usar herencia múltiple (cuando cada parte necesita comunicarse con las otras a través de this)
  • la herencia virtual de std :: exception ( ¿Es necesaria la herencia virtual para las excepciones? )
  • etc.

Algunas veces puedes usar composición, y otras veces el MI es mejor. El punto es: tienes una opción. Hágalo responsablemente (y revise su código).

5. Entonces, ¿debo hacer herencia múltiple?

La mayoría de las veces, en mi experiencia, no. MI no es la herramienta correcta, incluso si parece funcionar, porque puede ser utilizada por los perezosos para agrupar características sin darse cuenta de las consecuencias (como hacer un Caran Enginey un a Wheel).

Pero a veces sí. Y en ese momento, nada funcionará mejor que MI.

Pero debido a que MI es maloliente, prepárate para defender tu arquitectura en las revisiones de código (y defenderlo es algo bueno, porque si no eres capaz de defenderlo, entonces no debes hacerlo).

paercebal
fuente
44
Yo diría que nunca es necesario y tan raramente útil que, incluso si se ajusta perfectamente, los ahorros sobre la delegación no compensarán la confusión de los programadores que no están acostumbrados a usarlo, sino un buen resumen.
Bill K
2
Agregaría al punto 5, que el código que usa MI debe tener un comentario justo al lado explicando la razón, por lo que no tiene que explicarlo verbalmente en una revisión de código. Sin esto, alguien que no sea su revisor puede cuestionar su buen juicio cuando vean su código y es posible que no tenga la oportunidad de defenderlo.
Tim Abell
" porque no encontrarás el Diamante del terror descrito anteriormente " . ¿Por qué no?
curioso el
1
Yo uso MI para el patrón de observador.
Calmarius
13
@ Bill: Por supuesto que no es necesario. Si tiene un lenguaje Turning completo sin MI, puede hacer cualquier cosa que pueda hacer un idioma con MI. Entonces sí, no es necesario. Nunca. Dicho esto, puede ser una herramienta muy útil, y el llamado Diamante temido ... Nunca he entendido realmente por qué la palabra "temido" está ahí. En realidad, es bastante fácil razonar y generalmente no es un problema.
Thomas Eding
145

De una entrevista con Bjarne Stroustrup :

La gente dice correctamente que no necesita herencia múltiple, porque todo lo que puede hacer con herencia múltiple también puede hacerlo con herencia única. Simplemente usa el truco de delegación que mencioné. Además, no necesita ninguna herencia en absoluto, porque cualquier cosa que haga con herencia única también puede prescindir de la herencia mediante el reenvío a través de una clase. En realidad, tampoco necesita ninguna clase, porque puede hacerlo todo con punteros y estructuras de datos. ¿Pero por qué querrías hacer eso? ¿Cuándo es conveniente utilizar las instalaciones de idiomas? ¿Cuándo preferirías una solución alternativa? He visto casos en los que la herencia múltiple es útil, e incluso he visto casos en los que la herencia múltiple bastante complicada es útil. En general, prefiero usar las facilidades que ofrece el idioma para hacer soluciones

Nemanja Trifunovic
fuente
66
Hay algunas características de C ++ que me he perdido en C # y Java, aunque tenerlas haría que el diseño sea más difícil / complicado. Por ejemplo, las garantías de inmutabilidad de const: he tenido que escribir soluciones alternativas torpes (generalmente usando interfaces y composición) cuando una clase realmente necesitaba tener variantes mutables e inmutables. Sin embargo, nunca he una vez perdida la herencia múltiple, y nunca he sentido que tenía que escribir una solución debido a la falta de esta característica. Esa es la diferencia. En todos los casos que he visto, no usar MI es la mejor opción de diseño, no una solución alternativa.
BlueRaja - Danny Pflughoeft
24
+1: Respuesta perfecta: si no quieres usarlo, entonces no lo uses.
Thomas Eding
38

No hay razón para evitarlo y puede ser muy útil en situaciones. Sin embargo, debe ser consciente de los posibles problemas.

El más grande es el diamante de la muerte:

class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;

Ahora tiene dos "copias" de GrandParent dentro de Child.

Sin embargo, C ++ ha pensado en esto y le permite hacer una herencia virtual para solucionar los problemas.

class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;

Siempre revise su diseño, asegúrese de no estar usando la herencia para ahorrar en la reutilización de datos. Si puede representar lo mismo con la composición (y normalmente puede hacerlo), este es un enfoque mucho mejor.

billybob
fuente
21
Y si usted prohíbe la herencia y la única composición de uso en su lugar, siempre tiene dos GrandParenten Child. Existe un temor al infarto de miocardio, porque las personas simplemente piensan que podrían no entender las reglas del idioma. Pero cualquiera que no pueda obtener estas reglas simples tampoco puede escribir un programa no trivial.
curioso
1
Eso suena duro, las reglas de C ++ en general son muy elaboradas. Bien, incluso si las reglas individuales son simples, hay muchas de ellas, lo que hace que todo no sea simple, al menos para que lo siga un humano.
Pez cebra
11

Ver w: Herencia múltiple .

La herencia múltiple ha recibido críticas y, como tal, no se implementa en muchos idiomas. Las críticas incluyen:

  • Mayor complejidad
  • La ambigüedad semántica a menudo se resume como el problema del diamante .
  • No poder heredar explícitamente varias veces de una sola clase
  • Orden de herencia que cambia la semántica de clases.

La herencia múltiple en lenguajes con constructores de estilo C ++ / Java exacerba el problema de herencia de los constructores y el encadenamiento de constructores, creando así problemas de mantenimiento y extensibilidad en estos lenguajes. Los objetos en relaciones de herencia con métodos de construcción muy variables son difíciles de implementar bajo el paradigma de encadenamiento de constructores.

Manera moderna de resolver esto para usar la interfaz (clase abstracta pura) como la interfaz COM y Java.

¿Puedo hacer otras cosas en lugar de esto?

Sí tu puedes. Voy a robar de GoF .

  • Programa para una interfaz, no una implementación
  • Prefiere la composición sobre la herencia
Eugene Yokota
fuente
8

La herencia pública es una relación IS-A, y a veces una clase será un tipo de varias clases diferentes, y a veces es importante reflejar esto.

Las "mixinas" también son a veces útiles. En general, son clases pequeñas, que generalmente no se heredan de nada, lo que proporciona una funcionalidad útil.

Siempre que la jerarquía de herencia sea bastante superficial (como casi siempre debería ser) y esté bien administrada, es poco probable que obtenga la temida herencia de diamantes. El diamante no es un problema con todos los lenguajes que usan herencia múltiple, pero el tratamiento de C ++ es con frecuencia incómodo y a veces desconcertante.

Si bien me he encontrado con casos en los que la herencia múltiple es muy útil, en realidad son bastante raros. Esto es probable porque prefiero usar otros métodos de diseño cuando realmente no necesito herencia múltiple. Prefiero evitar confusos construcciones de lenguaje, y es fácil construir casos de herencia en los que tienes que leer el manual realmente bien para descubrir qué está pasando.

David Thornley
fuente
6

No debe "evitar" la herencia múltiple, pero debe tener en cuenta los problemas que pueden surgir, como el 'problema del diamante' ( http://en.wikipedia.org/wiki/Diamond_problem ) y tratar con cuidado el poder que se le otorga. , como deberías con todos los poderes.

jwpfox
fuente
1
+1: así como no deberías evitar los punteros; solo necesita estar al tanto de sus ramificaciones. Las personas deben dejar de recitar frases (como "MI es malo", "no optimizar", "goto es malo") que aprendieron en Internet solo porque algunos hippies dijeron que era malo para su higiene. La peor parte es que nunca intentaron usar tales cosas y simplemente lo tomaron como si se hablara de la Biblia.
Thomas Eding
1
Estoy de acuerdo. No lo "evite", pero conozca la mejor manera de manejar el problema. Tal vez prefiera usar rasgos de composición, pero C ++ no tiene eso. Tal vez prefiera usar la delegación automatizada 'maneja', pero C ++ no tiene eso. Así que uso la herramienta general y contundente de MI para varios propósitos diferentes, tal vez simultáneamente.
JDługosz
3

A riesgo de ser un poco abstracto, me resulta iluminador pensar en la herencia dentro del marco de la teoría de categorías.

Si pensamos en todas nuestras clases y flechas entre ellas que denotan relaciones de herencia, entonces algo como esto

A --> B

significa que class Bderiva de class A. Tenga en cuenta que, dado

A --> B, B --> C

decimos que C deriva de B que deriva de A, por lo que también se dice que C deriva de A, por lo tanto

A --> C

Además, decimos que para cada clase de la Aque Aderiva trivialmente A, nuestro modelo de herencia cumple con la definición de una categoría. En un lenguaje más tradicional, tenemos una categoría Classcon objetos de todas las clases y morfismos de las relaciones de herencia.

Eso es un poco de configuración, pero con eso echemos un vistazo a nuestro Diamond of Doom:

C --> D
^     ^
|     |
A --> B

Es un diagrama de aspecto sombrío, pero servirá. Así que Dhereda de todos A, By C. Además, y cada vez más cerca de abordar la pregunta de OP, Dtambién hereda de cualquier superclase de A. Podemos dibujar un diagrama

C --> D --> R
^     ^
|     |
A --> B
^ 
|
Q

Ahora, los problemas asociados con el Diamante de la Muerte aquí son cuándo Cy Bcompartir algunos nombres de propiedades / métodos y las cosas se vuelven ambiguas; sin embargo, si trasladamos cualquier comportamiento compartido, Ala ambigüedad desaparece.

En términos categóricos, queremos A, By Cser tales que si By Cheredar de Qentonces Ase puedan reescribir como una subclase de Q. Esto hace que Aalgo se llame pushout .

También hay una construcción simétrica Dllamada retroceso . Esta es esencialmente la clase útil más general que puede construir que hereda de ambos By C. Es decir, si tiene cualquier otra clase de Rherencia múltiple By C, entonces, Des una clase donde Rse puede reescribir como una subclase de D.

Asegurarse de que sus puntas del diamante sean retrocesos y flexiones nos brinda una buena manera de manejar genéricamente los problemas de nombres o de mantenimiento que podrían surgir de otra manera.

Tenga en cuenta que la respuesta de Paercebal inspiró esto, ya que sus advertencias están implícitas en el modelo anterior dado que trabajamos en la categoría de clase completa de todas las clases posibles.

Quería generalizar su argumento a algo que muestra cuán complicadas son las relaciones de herencia múltiple que pueden ser poderosas y no problemáticas.

TL; DR Piense en las relaciones de herencia en su programa como formando una categoría. Luego, puede evitar los problemas de Diamond of Doom al hacer que las clases heredadas se eliminen y simétricamente, creando una clase principal común que es un retroceso.

Ncat
fuente
3

Usamos Eiffel. Tenemos excelente MI. Sin preocupaciones. Sin problemas. Fácilmente gestionado. Hay momentos para NO usar MI. Sin embargo, es más útil de lo que las personas se dan cuenta porque están: A) en un lenguaje peligroso que no lo maneja bien -O BIEN- B) satisfecho con la forma en que han trabajado con MI durante años y años -O BIEN- C) otras razones ( demasiado numeroso para enumerarlo, estoy bastante seguro, vea las respuestas anteriores).

Para nosotros, usar Eiffel, MI es tan natural como cualquier otra cosa y otra buena herramienta en la caja de herramientas. Francamente, no nos preocupa que nadie más esté usando Eiffel. Sin preocupaciones. Estamos contentos con lo que tenemos y te invitamos a echar un vistazo.

Mientras busca: tome nota especial de la seguridad de vacío y la erradicación de la anulación de referencia de puntero nulo. ¡Mientras todos bailamos alrededor de MI, sus punteros se están perdiendo! :-)

Larry
fuente
2

Cada lenguaje de programación tiene un tratamiento ligeramente diferente de la programación orientada a objetos con pros y contras. La versión de C ++ pone el énfasis directamente en el rendimiento y tiene el inconveniente de que es inquietantemente fácil escribir código inválido, y esto es cierto para la herencia múltiple. Como consecuencia, hay una tendencia a alejar a los programadores de esta función.

Otras personas han abordado la pregunta de para qué no es buena la herencia múltiple. Pero hemos visto bastantes comentarios que implican más o menos que la razón para evitarlo es porque no es seguro. Pues sí y no.

Como suele suceder en C ++, si sigue una directriz básica, puede usarla de manera segura sin tener que "mirar por encima del hombro" constantemente. La idea clave es distinguir un tipo especial de definición de clase llamada "mezcla"; La clase es una combinación si todas sus funciones miembro son virtuales (o virtuales). Entonces se le permite heredar de una sola clase principal y tantos "mix-ins" como desee, pero debe heredar mixins con la palabra clave "virtual". p.ej

class CounterMixin {
    int count;
public:
    CounterMixin() : count( 0 ) {}
    virtual ~CounterMixin() {}
    virtual void increment() { count += 1; }
    virtual int getCount() { return count; }
};

class Foo : public Bar, virtual public CounterMixin { ..... };

Mi sugerencia es que si tiene la intención de usar una clase como una clase mixta, también adopta una convención de nomenclatura para facilitar que cualquiera que lea el código vea lo que está sucediendo y verifique que está jugando según las reglas de la guía básica. . Y encontrará que funciona mucho mejor si sus mezclas también tienen constructores predeterminados, solo por la forma en que funcionan las clases base virtuales. Y recuerda hacer todos los destructores virtuales también.

Tenga en cuenta que mi uso de la palabra "mezclar" aquí no es lo mismo que la clase de plantilla parametrizada (vea este enlace para una buena explicación) pero creo que es un uso justo de la terminología.

Ahora no quiero dar la impresión de que esta es la única forma de usar la herencia múltiple de forma segura. Es solo una forma que es bastante fácil de verificar.

sfkleach
fuente
2

Debe usarlo con cuidado, hay algunos casos, como el problema del diamante , en que las cosas pueden complicarse.

texto alternativo
(fuente: learncpp.com )

CMS
fuente
12
Este es un mal ejemplo, porque una copiadora debe estar compuesta de un escáner y una impresora, no heredar de ellos.
Svante el
2
De todos modos, a Printerni siquiera debería ser a PoweredDevice. A Printeres para imprimir, no para administrar la energía. La implementación de una impresora en particular podría tener que administrar algo de energía, pero estos comandos de administración de energía no deberían exponerse directamente a los usuarios de la impresora. No puedo imaginar el uso de esta jerarquía en el mundo real.
curioso
1

Más allá del patrón de diamante, la herencia múltiple tiende a dificultar la comprensión del modelo de objetos, lo que a su vez aumenta los costos de mantenimiento.

La composición es intrínsecamente fácil de entender, comprender y explicar. Puede ser tedioso escribir código, pero un buen IDE (han pasado algunos años desde que trabajé con Visual Studio, pero ciertamente los IDE de Java tienen excelentes herramientas de automatización de atajos de composición) deberían superar ese obstáculo.

Además, en términos de mantenimiento, el "problema del diamante" surge también en casos de herencia no literal. Por ejemplo, si tiene A y B y su clase C los extiende a ambos, y A tiene un método 'makeJuice' que hace jugo de naranja y usted lo extiende para hacer jugo de naranja con un toque de lima: ¿qué sucede cuando el diseñador de ' B 'agrega un método' makeJuice 'que genera y corriente eléctrica? 'A' y 'B' pueden ser "padres" compatibles en este momento , ¡pero eso no significa que siempre lo serán!

En general, la máxima de tender a evitar la herencia, y especialmente la herencia múltiple, es sólida. Como todas las máximas, hay excepciones, pero debe asegurarse de que haya un letrero de neón verde intermitente que señale cualquier excepción que codifique (y entrene su cerebro para que cada vez que vea tales árboles de herencia dibuje en su propio neón verde intermitente signo), y que verifique para asegurarse de que todo tenga sentido de vez en cuando.

Tom Dibble
fuente
what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?Uhhh, obtienes un error de compilación, por supuesto (si el uso es ambiguo).
Thomas Eding
1

El problema clave con MI de los objetos concretos es que rara vez tiene un objeto que legítimamente debería "ser un A y ser un B", por lo que rara vez es la solución correcta por razones lógicas. Con mucha más frecuencia, tiene un objeto C que obedece "C puede actuar como A o B", lo que puede lograr a través de la herencia y composición de la interfaz. Pero no se equivoque: la herencia de varias interfaces sigue siendo MI, solo un subconjunto.

Para C ++ en particular, la debilidad clave de la característica no es la EXISTENCIA real de herencia múltiple, pero algunas construcciones que permite casi siempre tienen malformaciones. Por ejemplo, heredar varias copias del mismo objeto como:

class B : public A, public A {};

está malformado POR DEFINICIÓN. Traducido al inglés, esto es "B es una A y una A". Entonces, incluso en el lenguaje humano hay una gran ambigüedad. ¿Quiso decir "B tiene 2 As" o simplemente "B es una A"? Permitir dicho código patológico, y lo que es peor, convertirlo en un ejemplo de uso, C ++ no favoreció a la hora de defender la función en lenguajes sucesores.

Zack Yezek
fuente
0

Puede usar la composición con preferencia a la herencia.

El sentimiento general es que la composición es mejor, y está muy bien discutida.

Ali Afshar
fuente
44
-1: The general feeling is that composition is better, and it's very well discussed.Eso no significa que la composición sea mejor.
Thomas Eding
Excepto que, en realidad, es mejor.
Ali Afshar
12
Excepto en los casos en que no es mejor.
Thomas Eding
0

toma 4/8 bytes por clase involucrada. (Uno este puntero por clase).

Esto podría nunca ser una preocupación, pero si algún día tiene una estructura de microdatos que se ejecuta miles de millones de veces, lo será.

Ronny Brendel
fuente
1
¿Seguro sobre eso? Típicamente, cualquier herencia seria requerirá un puntero en cada miembro de la clase, pero ¿eso necesariamente escala con las clases heredadas? Más concretamente, ¿cuántas veces ha tenido miles de millones de estructuras de datos pequeñas?
David Thornley el
3
@DavidThornley " ¿cuántas veces has tenido miles de millones de pequeñas estructuras de datos? " Cuando estás inventando argumentos en contra de MI.
curioso