2010-01-05 46 views
23

¿Cómo se alinean/ordenan los miembros de datos si se usa herencia/herencia múltiple? ¿Es este compilador específico?C++ alineación de datos/orden y herencia de miembros

¿Existe alguna manera de especificar en una clase derivada cómo se ordenarán/alinearán los miembros (incluidos los miembros de la clase base)?

Gracias!

+0

relacionada: http://stackoverflow.com/questions/2006216/why-is-data-structure-alignment-important-for-performance – jldupont

+0

Recuerde que el tamaño de una estructura (o clase) puede no será igual a la suma del tamaño de sus miembros, principalmente porque los compiladores pueden agregar relleno entre los miembros. Un método más robusto es leer datos empaquetados en un búfer 'unsigned char', luego cargar los miembros de ese búfer. En similar, escribe miembros en un búfer, el resultado es el búfer. Esto evitará que problemas de alineación o embalaje causen estragos en su programa. –

+0

no me preocupa el relleno, el relleno está bien, solo quiero poder predecir el formato de datos sin procesar de una estructura simple que es descendente de otras estructuras simples – Mat

Respuesta

56

Realmente hace muchas preguntas diferentes aquí, por lo que voy a hacer todo lo posible para responder a cada una por turno.

Primero quiere saber cómo están alineados los miembros de los datos. La alineación de miembros está definida por el compilador, pero debido a la forma en que las CPU tratan los datos desalineados, todos tienden a seguir la misma directriz

que las estructuras deben alinearse en función del miembro más restrictivo (que suele ser, aunque no siempre, el más grande). tipo intrínseco), y las estructuras siempre están alineadas de modo que los elementos de una matriz estén todos alineados de la misma manera.

Por ejemplo:

struct some_object 
{ 
    char c; 
    double d; 
    int i; 
}; 

Este struct sería 24 bytes. Debido a que la clase contiene un doble, estará alineada con 8 bytes, lo que significa que el char será rellenado por 7 bytes, y el int se rellenará por 4 para asegurar que en una matriz de some_object, todos los elementos estén alineados en 8 bytes. En general, esto depende del compilador, aunque encontrará que para una arquitectura de procesador dada, la mayoría de los compiladores alinean los datos de la misma manera.

Lo segundo que mencionas son los miembros derivados de la clase. Ordenar y alinear clases derivadas es un poco doloroso. Las clases siguen individualmente las reglas que describí anteriormente para las estructuras, pero cuando empiezas a hablar sobre la herencia te metes en un terreno desordenado. Dadas las siguientes clases:

class base 
{ 
    int i; 
}; 

class derived : public base // same for private inheritance 
{ 
    int k; 
}; 

class derived2 : public derived 
{ 
    int l; 
}; 

class derived3 : public derived, public derived2 
{ 
    int m; 
}; 

class derived4 : public virtual base 
{ 
    int n; 
}; 

class derived5 : public virtual base 
{ 
    int o; 
}; 

class derived6 : public derived4, public derived5 
{ 
    int p; 
}; 

el diseño de memoria para la base serían:

int i // base 

el diseño de memoria para derivado sería:

int i // base 
int k // derived 

el diseño de memoria para derived2 sería:

int i // base 
int k // derived 
int l // derived2 

El diseño de la memoria para derivado3 sería:

int i // base 
int k // derived 
int i // base 
int k // derived 
int l // derived2 
int m // derived3 

Puede notar que la base y los derivados aparecen cada uno aquí dos veces. Esa es la maravilla de la herencia múltiple.

Para evitar que tengamos herencia virtual.

el diseño de memoria para derived4 sería:

base* base_ptr // ptr to base object 
int n // derived4 
int i // base 

el diseño de memoria para derived5 sería:

base* base_ptr // ptr to base object 
int o // derived5 
int i // base 

el diseño de memoria para derived6 sería:

base* base_ptr // ptr to base object 
int n // derived4 
int o // derived5 
int i // base 

Usted Notará que los derivados 4, 5 y 6 tienen un puntero al objeto base. Esto es necesario para que al llamar a cualquiera de las funciones de la base tenga un objeto para pasar a esas funciones. Esta estructura depende del compilador porque no está especificada en la especificación del lenguaje, pero casi todos los compiladores la implementan de la misma manera.

Las cosas se vuelven más complicadas cuando empiezas a hablar de funciones virtuales, pero, de nuevo, la mayoría de los compiladores las implementan igual. Tome las siguientes clases:

class vbase 
{ 
    virtual void foo() {}; 
}; 

class vbase2 
{ 
    virtual void bar() {}; 
}; 

class vderived : public vbase 
{ 
    virtual void bar() {}; 
    virtual void bar2() {}; 
}; 

class vderived2 : public vbase, public vbase2 
{ 
}; 

Cada una de estas clases contiene al menos una función virtual.

el diseño de memoria para Vbase sería:

void* vfptr // vbase 

el diseño de memoria para vbase2 sería:

void* vfptr // vbase2 

el diseño de memoria para vderived sería:

void* vfptr // vderived 

El el diseño de la memoria para vderived2 sería:

void* vfptr // vbase 
void* vfptr // vbase2 

Hay muchas cosas que las personas no entienden sobre cómo funcionan los vftables. Lo primero que debe entender es que las clases solo almacenan punteros a vftables, no a vftables completos.

Lo que eso significa es que no importa cuántas funciones virtuales tenga una clase, solo tendrá una variable virtual, a menos que herede una variable virtual de otra parte a través de herencia múltiple. Casi todos los compiladores colocan el puntero vftable ante el resto de los miembros de la clase. Eso significa que puede tener algo de relleno entre el puntero de vftable y los miembros de la clase.

También puedo decirle que casi todos los compiladores implementan las capacidades del paquete pragma que le permiten forzar manualmente la alineación de la estructura. En general, no quieres hacer eso a menos que realmente sepas lo que estás haciendo, pero está allí, y algunas veces es necisario.

Lo último que ha preguntado es si puede controlar el pedido. Siempre controlas el orden. El compilador siempre ordenará las cosas en el orden en que las escriba. Espero que esta explicación larga alcance todo lo que necesita saber.

+1

+1 ¡Interesante! – DaMacc

+0

años - ¡esto es muy bueno! ¡muchas gracias! solo una pregunta más: ¿esas reglas también se mantienen si se definen constructores y destructores no predeterminados? – genesys

+1

Absolutamente. Los constructores y los destructores no tienen ningún efecto en el diseño de la clase a menos que el destructor sea virtual, en cuyo caso debe estar presente un vfptr. Además, no ingresé porque estaba un poco fuera del alcance de la pregunta, pero tenga cuidado con el orden de inicialización. La inicialización siempre ocurre desde una dirección de memoria baja a alta, excepto en el caso de la herencia virtual donde los objetos virtualmente heredados se construyen primero. De forma similar, los destinatarios se llaman desde la dirección alta a la baja, excepto en el caso de la herencia virtual, donde el destructor de la clase heredada virtual se llama último. – Beanz

1

Es específico del compilador.

Editar: básicamente se trata de donde se coloca la tabla virtual y que puede ser diferente dependiendo de qué compilador se utiliza.

+0

No, solo el relleno entre miembros es específico del compilador. El orden de los miembros está definido por las especificaciones del idioma. –

+0

La colocación de la tabla virtual es "indefinida". Solo necesita estar allí. En el pasado existía una diferencia entre cómo VC y GCC colocaban las tablas virtuales en varios casos de herencia ... – Goz

1

Tan pronto como su clase no sea POD (datos antiguos simples) todas las apuestas estarán desactivadas. Probablemente haya directivas específicas del compilador que puede usar para empaquetar/alinear datos.

+1

La preferencia no es usar las directivas del compilador para empaquetar la estructura, sino usar métodos para leer y escribir miembros en un búfer empaquetado. Esto proporciona un programa más robusto, especialmente cuando cambian el proveedor del compilador o las versiones. –

+0

Sí, pero tiene más código para mantener e introducir alcance para los errores si las estructuras cambian. Probabilidad es que sus estructuras cambien con más frecuencia de lo que lo hace su compilador;) Por supuesto, no hay fin de las estrategias de serialización/deserialización cuando comienza, diseñadas para resolver ambos problemas. – James

3

No es solo un compilador específico: es probable que se vea afectado por las opciones del compilador. No conozco ningún compilador que le brinde un control minucioso sobre cómo se empaquetan y se ordenan los miembros y las bases con herencia múltiple.

Si está haciendo algo que depende del orden y el embalaje, intente almacenar una estructura POD dentro de su clase y usar eso.

+0

las estructuras de datos en cuestión SON estructuras POD (al menos si la herencia múltiple de otras estructuras POD todavía muestra una estructura POD). Entonces, los miembros se les ordenará algo como 'basePODStruct1_members'-padding-'basePODStruct2_members'-padding -...' derivedPODStruct_members'-padding? – genesys

+0

@genesys: si hay herencia (o funciones virtuales, o constructores o destructores), entonces la estructura no es POD. –

+0

El relleno entre miembros es específico del compilador. El orden de los miembros está definido por la especificación del idioma. –

0

Todo el compilador que conozco puso el objeto de la clase base antes de los miembros de datos en un objeto de clase derivado. Los miembros de los datos están en orden como se indica en la declaración de la clase. Puede haber lagunas debido a la alineación. Sin embargo, no digo que tenga que ser así.

+0

El orden de los miembros está definido por la especificación del lenguaje, incluida la herencia. El relleno entre los miembros y las clases durante la herencia se define en la implementación. –

+0

solo por curiosidad: ¿cómo se produce el relleno no especificado por el idioma? Las estructuras – Mat

1

Los compiladores generalmente alinean los miembros de los datos en las estructuras para permitir un fácil acceso. Esto significa que los elementos de datos normalmente comenzarán en los límites de las palabras y que las brechas normalmente se dejarán en una estructura para garantizar que los límites de las palabras no estén divididos.

así

 
struct foo 
{ 
    char a; 
    int b; 
    char c; 
} 

normalmente tomar hasta más de 6 bytes para una máquina de 32 bits

La clase base es normalmente organizada primera y la clase derivada que organizada después de la clase base. Esto permite que la dirección de la clase base sea igual a la dirección de la clase derivada.

En la herencia múltiple hay un desplazamiento entre la dirección de una clase y la dirección de la segunda clase base. >static_cast y dynamic_cast calcularán el desplazamiento. reinterpret_cast no. Los moldes de estilo C hacen un yeso estático si es posible, de lo contrario se vuelve a interpretar el reparto.

Como han mencionado otros, todo esto es específico del compilador, pero lo anterior debería darle una guía aproximada de lo que normalmente sucede.

+0

no se tratan de manera diferente a las clases para la alineación y el empaque. Lo importante es si la estructura/clase es POD o no. –

+0

Puede haber un desplazamiento incluso con herencia simple. Cuando una clase con funciones virtuales se deriva de un tipo POD, los compiladores populares organizarán la memoria como 'vtableptr + POD + derived' –

1

El orden de los objetos en herencia múltiple no es siempre lo que usted especifica. Según lo que he experimentado, el compilador utilizará el orden especificado a menos que no pueda. No puede usar el orden especificado cuando la primera clase base no tiene funciones virtuales y otra clase base tiene funciones virtuales. En este caso, los primeros bytes de la clase tienen que ser un puntero de tabla de funciones virtuales, pero la primera clase base no tiene uno. El compilador reorganizará las clases base para que el primero tenga un puntero de tabla de funciones virtuales.

He probado esto con msdev y g ++ y ambos reorganizan las clases. Es molesto que parezcan tener reglas diferentes sobre cómo lo hacen. Si tiene 3 o más clases base y la primera no tiene funciones virtuales, estos compiladores presentarán diferentes diseños.

Para estar seguro, elija dos y evite el otro.

  1. No confíe en el orden de las clases base cuando usa herencia múltiple.

  2. Al usar la herencia múltiple, coloque todas las clases base con funciones virtuales antes de cualquier clase base sin funciones virtuales.

  3. clases de uso de 2 o menos de base (ya que los compiladores tanto se reordenan de la misma manera que en este caso)

0

puedo responder una de las preguntas.

¿Cómo se alinean/ordenan los miembros de datos si se utiliza herencia/herencia múltiple?

He creado una herramienta para visualizar el diseño de memoria de clases, marcos de pila de funciones y otra información de ABI (Linux, GCC). Puede ver el resultado de la clase mysqlpp :: Connection (hereda OptionalExceptions) de la biblioteca MySQL ++ here.

enter image description here