resolucion - Tamaño del objeto C++ con métodos virtuales
operador de resolucion de ambito c++ (6)
Tengo algunas preguntas sobre el tamaño del objeto con virtual.
1) función virtual
class A {
public:
int a;
virtual void v();
}
El tamaño de la clase A es de 8 bytes ... un entero (4 bytes) más un puntero virtual (4 bytes) ¡Está claro!
class B: public A{
public:
int b;
virtual void w();
}
¿Cuál es el tamaño de la clase B? Probé usando sizeof B, imprime 12
¿Significa que solo hay un vptr, incluso los dos de clase B y clase A tienen función virtual? ¿Por qué solo hay un vptr?
class A {
public:
int a;
virtual void v();
};
class B {
public:
int b;
virtual void w();
};
class C : public A, public B {
public:
int c;
virtual void x();
};
El tamaño de C es 20 ........
Parece que en este caso, dos vptrs están en el diseño ..... ¿Cómo sucede esto? Creo que los dos vptrs uno es para la clase A y otro es para la clase B .... por lo que no hay vptr para la función virtual de la clase C?
Mi pregunta es, ¿cuál es la regla sobre el número de vptrs en herencia?
2) herencia virtual
class A {
public:
int a;
virtual void v();
};
class B: virtual public A{ //virtual inheritance
public:
int b;
virtual void w();
};
class C : public A { //non-virtual inheritance
public:
int c;
virtual void x();
};
class D: public B, public C {
public:
int d;
virtual void y();
};
El tamaño de A es de 8 bytes -------------- 4 (int a) + 4 (vptr) = 8
El tamaño de B es de 16 bytes -------------- Sin virtual, debe ser 4 + 4 + 4 = 12. ¿Por qué hay otros 4 bytes aquí? ¿Cuál es el diseño de la clase B?
El tamaño de C es de 12 bytes. -------------- 4 + 4 + 4 = 12. ¡Está claro!
El tamaño de D es de 32 bytes -------------- debe ser 16 (clase B) + 12 (clase C) + 4 (int d) = 32. ¿Es así?
class A {
public:
int a;
virtual void v();
};
class B: virtual public A{ //virtual inheritance here
public:
int b;
virtual void w();
};
class C : virtual public A { //virtual inheritance here
public:
int c;
virtual void x();
};
class D: public B, public C {
public:
int d;
virtual void y();
};
sizeof A es 8
sizeof B es 16
sizeof C es 16
sizeof D es 28 ¿Significa 28 = 16 (clase B) + 16 (clase C) - 8 (clase A) + 4 (¿qué es esto?)
Mi pregunta es, ¿por qué hay un espacio adicional cuando se aplica la herencia virtual?
¿Cuál es la regla subyacente para el tamaño del objeto en este caso?
¿Cuál es la diferencia cuando se aplica virtual en todas las clases base y en parte de las clases base?
Citar> Mi pregunta es, ¿cuál es la regla sobre el número de vptrs en la herencia?
No hay rulez, cada proveedor de compiladores tiene permitido implementar la semántica de la herencia de la forma que crea conveniente.
clase B: público A {}, tamaño = 12. Eso es bastante normal, uno vtable para B que tiene ambos métodos virtuales, puntero vtable + 2 * int = 12
clase C: pública A, pública B {}, tamaño = 20. C puede extender arbitrariamente la tabla variable de A o B. 2 * puntero vtable + 3 * int = 20
Herencia virtual: ahí es donde realmente tocas el borde del comportamiento indocumentado. Por ejemplo, en MSVC las opciones de compilación #pragma vtordisp y / vd se vuelven relevantes. Hay información de fondo en este artículo . Estudié esto un par de veces y decidí que el acrónimo de la opción de compilación era representativo de lo que podría pasar con mi código si alguna vez lo usé.
Esta es toda la implementación definida. Estoy usando VC10 Beta2. La clave para ayudar a entender esto (la implementación de funciones virtuales) es que debe conocer un interruptor secreto en el compilador de Visual Studio, / d1reportSingleClassLayoutXXX . Voy a llegar a eso en un segundo.
La regla básica es que el vtable debe ubicarse en el desplazamiento 0 para cualquier puntero a un objeto. Esto implica múltiples tablas virtuales para herencia múltiple.
Al par de preguntas aquí, comenzaré en la parte superior:
¿Significa que solo hay un vptr, incluso los dos de clase B y clase A tienen función virtual? ¿Por qué solo hay un vptr?
Así es como funcionan las funciones virtuales, quiere que la clase base y la clase derivada compartan el mismo puntero vtable (apuntando a la implementación en la clase derivada.
Parece que en este caso, dos vptrs están en el diseño ..... ¿Cómo sucede esto? Creo que los dos vptrs uno es para la clase A y otro es para la clase B .... por lo que no hay vptr para la función virtual de la clase C?
Este es el diseño de la clase C, según lo informado por / d1reportSingleClassLayoutC:
class C size(20):
+---
| +--- (base class A)
0 | | {vfptr}
4 | | a
| +---
| +--- (base class B)
8 | | {vfptr}
12 | | b
| +---
16 | c
+---
Tienes razón, hay dos tablas virtuales, una para cada clase base. Así es como funciona en herencia múltiple; si el C * se convierte a B *, el valor del puntero se ajusta en 8 bytes. Un vtable todavía tiene que estar en el offset 0 para que las llamadas a funciones virtuales funcionen.
El vtable en el diseño anterior para la clase A se trata como vtable de clase C (cuando se llama a través de C *).
El tamaño de B es de 16 bytes -------------- Sin virtual, debe ser 4 + 4 + 4 = 12. ¿Por qué hay otros 4 bytes aquí? ¿Cuál es el diseño de la clase B?
Este es el diseño de la clase B en este ejemplo:
class B size(20):
+---
0 | {vfptr}
4 | {vbptr}
8 | b
+---
+--- (virtual base A)
12 | {vfptr}
16 | a
+---
Como puede ver, hay un puntero adicional para manejar la herencia virtual. La herencia virtual es complicada.
El tamaño de D es de 32 bytes -------------- debe ser 16 (clase B) + 12 (clase C) + 4 (int d) = 32. ¿Es así?
No, 36 bytes. El mismo trato con la herencia virtual. Diseño de D en este ejemplo:
class D size(36):
+---
| +--- (base class B)
0 | | {vfptr}
4 | | {vbptr}
8 | | b
| +---
| +--- (base class C)
| | +--- (base class A)
12 | | | {vfptr}
16 | | | a
| | +---
20 | | c
| +---
24 | d
+---
+--- (virtual base A)
28 | {vfptr}
32 | a
+---
Mi pregunta es, ¿por qué hay un espacio adicional cuando se aplica la herencia virtual?
Puntero de clase base virtual, es complicado. Las clases base se "combinan" en herencia virtual. En lugar de tener una clase base incrustada en una clase, la clase tendrá un puntero al objeto de la clase base en el diseño. Si tiene dos clases base que usan herencia virtual (la jerarquía de clases "diamante"), ambas señalarán la misma clase base virtual en el objeto, en lugar de tener una copia separada de esa clase base.
¿Cuál es la regla subyacente para el tamaño del objeto en este caso?
Punto importante; no hay reglas: el compilador puede hacer lo que necesite hacer.
Y un detalle final; para hacer todos estos diagramas de diseño de clases con los que estoy compilando:
cl test.cpp /d1reportSingleClassLayoutXXX
Donde XXX es una coincidencia de subcadenas de las estructuras / clases que desea ver el diseño de. Al usar esto puede explorar los efectos de varios esquemas de herencia usted mismo, así como por qué / dónde se agrega el relleno, etc.
Esto obviamente depende de la implementación del compilador. De todos modos, creo que puedo resumir las siguientes reglas de la implementación dada por un documento clásico vinculado a continuación y que da la cantidad de bytes que obtienes en tus ejemplos (¡excepto para la clase D que sería de 36 bytes y no de 32!) :
El tamaño de un objeto de clase T es:
- El tamaño de sus campos MÁS la suma del tamaño de cada objeto del cual T hereda MÁS 4 bytes para cada objeto del cual T hereda virtualmente MÁS 4 bytes SÓLO SI T necesita OTRO v-table
- Preste atención: si una clase K virtualmente se hereda varias veces (en cualquier nivel), debe agregar el tamaño de K una sola vez
Entonces, tenemos que responder otra pregunta: ¿Cuándo necesita una clase OTRO v-table?
- Una clase que no hereda de otras clases necesita una tabla v solo si tiene uno o más métodos virtuales
- DE OTRA MANERA, una clase necesita otra tabla v SOLAMENTE SI NINGUNA de las clases de las que no hereda prácticamente tiene una tabla v
El final de las reglas (que creo que se puede aplicar para que coincida con lo que Terry Mahaffey ha explicado en su respuesta) :)
De todos modos mi sugerencia es leer el siguiente artículo de Bjarne Stroustrup (el creador de C ++) que explica exactamente estas cosas: cuántas tablas virtuales se necesitan con herencia virtual o no virtual ... ¡y por qué!
Realmente es una buena lectura: http://www.hpc.unimelb.edu.au/nec/g1af05e/chap5.html
No estoy seguro, pero creo que es por el puntero a la tabla de métodos virtuales
Todo esto está completamente definido por la implementación, te das cuenta. No puedes contar con nada de eso. No hay ''regla''.
En el ejemplo de herencia, así es como podría verse la tabla virtual para las clases A y B:
class A
+-----------------+
| pointer to A::v |
+-----------------+
class B
+-----------------+
| pointer to A::v |
+-----------------+
| pointer to B::w |
+-----------------+
Como puede ver, si tiene un puntero a la tabla virtual de la clase B, también es perfectamente válido como la tabla virtual de la clase A.
En su ejemplo de clase C, si lo piensa, no hay forma de hacer una tabla virtual que sea válida como una tabla para la clase C, la clase A y la clase B. Entonces, el compilador crea dos. Una tabla virtual es válida para las clases A y C (la mayoría probable) y la otra es válida para las clases A y B.
Una buena forma de pensar sobre esto es comprender qué se debe hacer para manejar los retratos. Intentaré responder a sus preguntas mostrando el diseño de memoria de los objetos de las clases que describe.
Muestra de código n. ° 2
El diseño de la memoria es el siguiente:
vptr | A::a | B::b
Actualizar un puntero a B para escribir A dará como resultado la misma dirección, con el mismo vptr que se usa. Esta es la razón por la cual no hay necesidad de vptr adicionales aquí.
Ejemplo de código n. ° 3
vptr | A::a | vptr | B::b | C::c
Como puede ver, hay dos vptr aquí, tal como lo adivinó. ¿Por qué? Porque es cierto que si cambiamos de C a A no tenemos que modificar la dirección y, por lo tanto, podemos usar el mismo vptr. Pero si cambiamos de C a B, necesitamos esa modificación y, en consecuencia, necesitamos un vptr al inicio del objeto resultante.
Por lo tanto, cualquier clase heredada más allá de la primera requerirá un vptr adicional (a menos que esa clase heredada no tenga métodos virtuales, en cuyo caso no tiene vptr).
Ejemplo de código n. ° 4 y más
Cuando obtiene virtualmente, necesita un nuevo puntero, llamado puntero base , para señalar la ubicación en el diseño de la memoria de las clases derivadas. Puede haber más de un puntero base, por supuesto.
Entonces, ¿cómo se ve el diseño de la memoria? Eso depende del compilador. En tu compilador, es probablemente algo así como
vptr | base pointer | B::b | vptr | A::a | C::c | vptr | A::a /-----------------------------------------^
Pero otros compiladores pueden incorporar punteros base en la tabla virtual (mediante el uso de compensaciones, que merece otra pregunta).
Necesita un puntero base porque cuando obtiene de forma virtual, la clase derivada aparecerá una sola vez en el diseño de la memoria (puede aparecer veces adicionales si también se deriva normalmente, como en su ejemplo), por lo que todos sus hijos deben apuntar a la misma ubicación exacta.
EDITAR: aclaración: todo depende realmente del compilador, el diseño de memoria que mostré puede ser diferente en diferentes compiladores.