poo - herencia c++ constructores
¿Existe alguna penalización/costo de la herencia virtual en C++, al llamar al método base no virtual? (6)
Al menos en una implementación típica, la herencia virtual conlleva una penalización (¡pequeña!) Para (al menos algunos) acceso a los miembros de datos. En particular, normalmente terminas con un nivel adicional de direccionamiento indirecto para acceder a los miembros de datos del objeto del cual derivaste virtualmente. Esto ocurre porque (al menos en el caso normal) dos o más clases derivadas separadas no solo tienen la misma clase base, sino el mismo objeto de clase base. Para lograr esto, ambas clases derivadas tienen punteros al mismo desplazamiento en el objeto más derivado y acceden a esos miembros de datos a través de ese puntero.
Aunque técnicamente no se debe a la herencia virtual, probablemente vale la pena señalar que hay una multa separada (de nuevo, pequeña) para la herencia múltiple en general. En una implementación típica de herencia única , tiene un puntero vtable en algún desplazamiento fijo en el objeto (muy a menudo al principio). En el caso de la herencia múltiple, obviamente no puede tener dos punteros vtable en el mismo desplazamiento, por lo que terminará con un número de punteros vtable, cada uno con un desplazamiento diferente en el objeto.
IOW, el puntero vtable con herencia única es normalmente static_cast<vtable_ptr_t>(object_address)
, pero con herencia múltiple obtiene static_cast<vtable_ptr_t>(object_address+offset)
.
Técnicamente, los dos están completamente separados, pero, por supuesto, casi el único uso de la herencia virtual es en conjunto con la herencia múltiple, por lo que es semi-relevante de todos modos.
¿El uso de la herencia virtual en C ++ tiene una penalización de tiempo de ejecución en el código compilado, cuando llamamos a un miembro de función regular desde su clase base? Código de muestra:
class A {
public:
void foo(void) {}
};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
// ...
D bar;
bar.foo ();
Concretamente, en Microsoft Visual C ++ hay una diferencia real en el tamaño de puntero a miembro. Ver #pragma pointers_to_members . Como puede ver en esa lista, el método más general es la "herencia virtual", que es distinta de la herencia múltiple, que a su vez es distinta de la herencia única.
Eso implica que se necesita más información para resolver un puntero a miembro en el caso de presencia de herencia virtual, y tendrá un impacto en el rendimiento aunque solo sea a través de la cantidad de datos recogidos en la memoria caché de la CPU, aunque probablemente también en el Longitud de la búsqueda del miembro o el número de saltos necesarios.
Creo que no hay penalización en tiempo de ejecución para la herencia virtual. No confunda herencia virtual con funciones virtuales. Ambas son dos cosas diferentes.
la herencia virtual garantiza que solo haya un subobjeto A
en instancias de D
Así que no creo que haya una penalización en el tiempo de ejecución solo por ello.
Sin embargo, pueden surgir casos en los que este subobjeto no se pueda conocer en el momento de la compilación, por lo que, en tales casos, habría una penalización en tiempo de ejecución por herencia virtual. James describe uno de estos casos en su respuesta.
Puede haber, sí, si llama a la función miembro a través de un puntero o referencia y el compilador no puede determinar con absoluta certeza a qué tipo de objeto se refiere ese puntero o puntos de referencia. Por ejemplo, considere:
void f(B* p) { p->foo(); }
void g()
{
D bar;
f(&bar);
}
Suponiendo que la llamada a f
no esté en línea, el compilador necesita generar código para encontrar la ubicación del subobjeto de clase base virtual A
para llamar a foo
. Por lo general, esta búsqueda implica la comprobación de vptr / vtable.
Sin embargo, si el compilador conoce el tipo de objeto sobre el que está llamando a la función (como es el caso en su ejemplo), no debería haber sobrecarga porque la llamada a la función puede enviarse estáticamente (en tiempo de compilación). En su ejemplo, se sabe que el tipo dinámico de bar
es D
(no puede ser otra cosa), por lo que el desplazamiento del subobjeto de clase base virtual A
se puede calcular en el momento de la compilación.
Sí, la herencia virtual tiene una sobrecarga de rendimiento en tiempo de ejecución. Esto se debe a que el compilador, para cualquier puntero / referencia a objeto, no puede encontrar sus subobjetos en tiempo de compilación. En contraste, para una herencia única, cada subobjeto se encuentra en un desplazamiento estático del objeto original. Considerar:
class A { ... };
class B : public A { ... }
El diseño de memoria de B se ve un poco así:
| B''s stuff | A''s stuff |
En este caso, el compilador sabe dónde está A. Sin embargo, ahora consideremos el caso de MVI.
class A { ... };
class B : public virtual A { ... };
class C : public virtual A { ... };
class D : public C, public B { ... };
Disposición de memoria de B:
| B''s stuff | A''s stuff |
Disposición de memoria de C:
| C''s stuff | A''s stuff |
¡Pero espera! Cuando D se crea una instancia, no se ve así.
| D''s stuff | B''s stuff | C''s stuff | A''s stuff |
Ahora, si tiene una B *, si realmente apunta a una B, entonces A está justo al lado de la B- pero si apunta a una D, entonces para obtener A * realmente necesita saltarse la sub C -objeto, y dado que cualquier B*
dado podría apuntar a B o a D dinámicamente en tiempo de ejecución, entonces deberá modificar el puntero dinámicamente. Esto, como mínimo, significa que tendrá que generar código para encontrar ese valor por algún medio, en lugar de tener el valor incorporado en el momento de la compilación, que es lo que ocurre con la herencia individual.
Su pregunta se centra principalmente en llamar a las funciones regulares de la base virtual, no en el caso (más) interesante de las funciones virtuales de la clase base virtual (clase A en su ejemplo), pero sí, puede haber un costo. Por supuesto todo depende del compilador.
Cuando el compilador compiló A :: foo, asumió que "esto" apunta al comienzo de donde los miembros de datos para A residen en la memoria. En este momento, es posible que el compilador no sepa que la clase A será una base virtual de cualquier otra clase. Pero felizmente genera el código.
Ahora, cuando el compilador compila B, no habrá realmente un cambio porque A es una clase base virtual, aún es una herencia simple y, en el caso típico, el compilador diseñará la clase B colocando a los miembros de datos de la clase A inmediatamente seguidos por los miembros de datos de la clase B, por lo que una B * se puede convertir inmediatamente en una A * sin ningún cambio en el valor, y por lo tanto, no es necesario realizar ningún ajuste. El compilador puede llamar a A :: foo usando el mismo puntero "this" (aunque es de tipo B *) y no hay daño.
La misma situación es para la clase C: su herencia aún es única, y el compilador típico colocará a los miembros de datos de A inmediatamente seguidos por los miembros de datos de C, de modo que un C * se pueda convertir inmediatamente en un A * sin ningún cambio en el valor. Por lo tanto, el compilador simplemente puede llamar a A :: foo con el mismo puntero "this" (aunque es de tipo C *) y no hay daño.
Sin embargo, la situación es totalmente diferente para la clase D. El diseño de la clase D normalmente será los miembros de datos de la clase A, seguidos por los miembros de datos de la clase B, seguidos por los miembros de datos de la clase C, seguidos por los miembros de datos de la clase D.
Usando el diseño típico, una D * se puede convertir inmediatamente a A *, por lo que no hay penalización para A :: foo-- el compilador puede llamar a la misma rutina que generó para A :: foo sin ningún cambio en "this" y todo está bien.
Sin embargo, la situación cambia si el compilador necesita llamar a una función miembro como C :: other_member_func, incluso si C :: other_member_func no es virtual. La razón es que cuando el compilador escribió el código para C :: other_member_func, asumió que el diseño de datos al que hace referencia el puntero "this" son los miembros de datos de A seguidos inmediatamente por los miembros de datos de C. Pero eso no es cierto para una instancia de D. El compilador puede necesitar volver a escribir y crear una D :: other_member_func (no virtual), solo para cuidar la diferencia de diseño de memoria de instancia de clase.
Tenga en cuenta que esta es una situación diferente pero similar al usar la herencia múltiple, pero en la herencia múltiple sin bases virtuales, el compilador puede encargarse de todo simplemente agregando un desplazamiento o corrección al puntero "this" para tener en cuenta dónde se encuentra una clase base "incrustado" dentro de una instancia de una clase derivada. Pero con las bases virtuales, a veces se necesita una reescritura de la función. Todo depende de a qué miembros de datos se accede mediante la función de miembro (incluso no virtual) a la que se llama.
Por ejemplo, si la clase C definió una función miembro no virtual C :: some_member_func, el compilador podría necesitar escribir:
- C :: some_member_func cuando se llama desde una instancia real de C (y no D), según se determina en el momento de la compilación (porque some_member_func no es una función virtual)
- C :: some_member_func cuando se llama a la misma función miembro desde una instancia real de la clase D, según se determina en el momento de la compilación. (Técnicamente, esta rutina es D :: some_member_func. Aunque la definición de esta función miembro es implícita e idéntica al código fuente de C :: some_member_func, el código objeto generado puede ser ligeramente diferente).
si el código para C :: some_member_func utiliza las variables miembro definidas tanto en la clase A como en la clase C.