c++ offsetof

¿Por qué no puedes usar offsetof en estructuras que no sean POD en C++?



(10)

Estaba investigando cómo obtener la compensación de memoria de un miembro de una clase en C ++ y encontré esto en wikipedia:

En el código C ++, no puede usar offsetof para acceder a miembros de estructuras o clases que no sean estructuras de datos antiguas sin formato.

Lo probé y parece funcionar bien.

class Foo { private: int z; int func() {cout << "this is just filler" << endl; return 0;} public: int x; int y; Foo* f; bool returnTrue() { return false; } }; int main() { cout << offsetof(Foo, x) << " " << offsetof(Foo, y) << " " << offsetof(Foo, f); return 0; }

Recibí algunas advertencias, pero se compiló y cuando se ejecutó dio un resultado razonable:

Laptop:test alex$ ./test 4 8 12

Creo que estoy malinterpretando lo que es una estructura de datos POD o me falta alguna otra pieza del rompecabezas. No veo cuál es el problema.


Apuesto a que compilas esto con VC ++. Ahora pruébelo con g ++, y vea cómo funciona ...

Para resumir, no está definido, pero algunos compiladores pueden permitirlo. Otros no lo hacen. En cualquier caso, no es portátil.


Creo que su clase se ajusta a la definición c ++ 0x de un POD. g ++ ha implementado algo de c ++ 0x en sus últimas versiones. Creo que VS2008 también tiene algunos bits c ++ 0x.

Del artículo c ++ 0x de wikipedia

C ++ 0x relajará varias reglas con respecto a la definición de POD.

Una clase / estructura se considera un POD si es trivial, un diseño estándar y si todos sus miembros no estáticos son POD.

Una clase o estructura trivial se define como una que:

  1. Tiene un constructor predeterminado trivial. Esto puede usar la sintaxis del constructor por defecto (SomeConstructor () = default;).
  2. Tiene un constructor de copia trivial, que puede usar la sintaxis predeterminada.
  3. Tiene un operador de asignación de copia trivial, que puede usar la sintaxis predeterminada.
  4. Tiene un destructor trivial, que no debe ser virtual.

Una clase o estructura de diseño estándar se define como una que:

  1. Solo tiene miembros de datos no estáticos que son de tipo de diseño estándar
  2. Tiene el mismo control de acceso (público, privado, protegido) para todos los miembros no estáticos
  3. No tiene funciones virtuales
  4. No tiene clases base virtuales
  5. Tiene solo clases base que son de tipo de diseño estándar
  6. No tiene clases base del mismo tipo que el primer miembro no estático definido
  7. O bien no tiene clases base con miembros no estáticos, o no tiene miembros de datos no estáticos en la clase más derivada y, como máximo, una clase base con miembros no estáticos. En esencia, puede haber solo una clase en la jerarquía de esta clase que tenga miembros no estáticos.

En C ++ puede obtener el desplazamiento relativo de esta manera:

class A { public: int i; }; class B : public A { public: int i; }; void test() { printf("%p, %p/n", &A::i, &B::i); // edit: changed %x to %p }


En general, cuando pregunta " por qué algo no está definido ", la respuesta es " porque el estándar así lo dice ". Por lo general, lo racional es por una o más razones, como:

  • es difícil de detectar estáticamente, en cuyo caso usted es.

  • los casos de esquina son difíciles de definir y nadie se tomó el trabajo de definir casos especiales;

  • su uso está cubierto en su mayoría por otras características;

  • las prácticas existentes en el momento de la estandarización variaban y la eliminación de la implementación existente y de los programas que dependían de ellas se consideraba más dañina que la normalización.

Volviendo a offsetof, la segunda razón es probablemente una dominante. Si nos fijamos en C ++ 0X, donde el estándar estaba usando POD, ahora está usando "diseño estándar", "diseño compatible", "POD", permitiendo casos más refinados. Y offsetof ahora necesita clases de "diseño estándar", que son los casos en los que el comité no quería forzar un diseño.

También debe considerar el uso común de offsetof (), que es obtener el valor de un campo cuando tiene un puntero void * al objeto. La herencia múltiple, virtual o no, es problemática para ese uso.


Esto parece funcionar bien para mí:

#define myOffset(Class,Member) ({Class o; (size_t)&(o.Member) - (size_t)&o;})


Funciona para mi

#define get_offset(type, member) ((size_t)(&((type*)(1))->member)-1) #define get_container(ptr, type, member) ((type *)((char *)(ptr) - get_offset(type, member)))


La respuesta de Bluehorn es correcta, pero para mí no explica la razón del problema en términos más simples. La forma en que lo entiendo es la siguiente:

Si NonPOD no es una clase POD, cuando lo haga:

NonPOD np; np.field;

el compilador no necesariamente accede al campo agregando algún desplazamiento al puntero base y desreferenciación. Para una clase POD, el Estándar C ++ lo restringe para hacer eso (o algo equivalente), pero para una clase que no es POD no lo hace. En su lugar, el compilador podría leer un puntero del objeto, agregar un desplazamiento a ese valor para dar la ubicación de almacenamiento del campo y luego eliminar la referencia. Este es un mecanismo común con herencia virtual si el campo es miembro de una base virtual de NonPOD. Pero no está restringido a ese caso. El compilador puede hacer prácticamente todo lo que quiera. Podría llamar a una función de miembro virtual generada por el compilador oculto si así lo desea.

En los casos complejos, obviamente no es posible representar la ubicación del campo como un desplazamiento entero. Entonces offsetof no es válido en clases que no sean POD.

En los casos en los que su compilador almacena el objeto de manera simple (como la herencia individual, y normalmente incluso la herencia múltiple no virtual, y normalmente los campos definidos en la clase a la que hace referencia al objeto por oposición a en alguna clase base), entonces funcionará. Probablemente hay casos que funcionan en cada compilador. Esto no lo hace válido.

Apéndice: ¿cómo funciona la herencia virtual?

Con la herencia simple, si B se deriva de A, la implementación habitual es que un puntero a B es solo un puntero a A, con los datos adicionales de B pegados al final:

A* ---> field of A <--- B* field of A field of B

Con la herencia múltiple simple, generalmente se asume que las clases base de B (call ''em A1 y A2) están organizadas en un orden peculiar a B. Pero el mismo truco con los punteros no puede funcionar:

A1* ---> field of A1 field of A1 A2* ---> field of A2 field of A2

A1 y A2 "no saben" nada sobre el hecho de que ambas son clases base de B. Entonces si lanzas un B * a A1 *, tiene que apuntar a los campos de A1, y si lo lanzas a A2 * tiene que apuntar a los campos de A2. El operador de conversión de puntero aplica un desplazamiento. Entonces podrías terminar con esto:

A1* ---> field of A1 <---- B* field of A1 A2* ---> field of A2 field of A2 field of B field of B

Luego, al convertir un B * en A1 * no se cambia el valor del puntero, pero al convertirlo a A2 * se agrega un sizeof(A1) bytes. Esta es la "otra" razón por la cual, en ausencia de un destructor virtual, borrar B a través de un puntero a A2 falla. No solo no llama al destructor de B y A1, ni siquiera libera la dirección correcta.

De todos modos, B "sabe" dónde están todas sus clases base, siempre se almacenan en las mismas compensaciones. Entonces, en esta disposición, la compensación de todavía funcionaría. El estándar no requiere implementaciones para hacer herencia múltiple de esta manera, pero a menudo lo hacen (o algo así). Así que offsetof podría funcionar en este caso en su implementación, pero no está garantizado.

Ahora, ¿qué pasa con la herencia virtual? Supongamos que B1 y B2 tienen A como una base virtual. Esto los convierte en clases de herencia única, por lo que podría pensar que el primer truco funcionará de nuevo:

A* ---> field of A <--- B1* A* ---> field of A <--- B2* field of A field of A field of B1 field of B2

Pero espera. ¿Qué sucede cuando C deriva (no virtualmente, por simplicidad) de B1 y B2? C solo debe contener 1 copia de los campos de A. Esos campos no pueden preceder inmediatamente a los campos de B1, y también inmediatamente preceden a los campos de B2. Estamos en problemas.

Entonces, lo que las implementaciones podrían hacer en su lugar es:

// an instance of B1 looks like this, and B2 similar A* ---> field of A field of A B1* ---> pointer to A field of B1

Aunque he indicado B1 * apuntando a la primera parte del objeto después del subobjeto A, sospecho (sin molestarme) que la dirección real no estará allí, será el comienzo de A. Es solo que a diferencia herencia simple, las compensaciones entre la dirección real en el puntero, y la dirección que he indicado en el diagrama, nunca se usarán a menos que el compilador tenga certeza del tipo dinámico del objeto. En cambio, siempre irá a través de la metainformación para llegar a A correctamente. Entonces, mis diagramas apuntarán allí, ya que esa compensación siempre se aplicará a los usos en los que estamos interesados.

El "puntero" a A podría ser un puntero o un desplazamiento, realmente no importa. En una instancia de B1, creada como B1, apunta a (char*)this - sizeof(A) , y lo mismo en una instancia de B2. Pero si creamos una C, puede verse así:

A* ---> field of A field of A B1* ---> pointer to A // points to (char*)(this) - sizeof(A) as before field of B1 B2* ---> pointer to A // points to (char*)(this) - sizeof(A) - sizeof(B1) field of B2 C* ----> pointer to A // points to (char*)(this) - sizeof(A) - sizeof(B1) - sizeof(B2) field of C field of C

Entonces, para acceder a un campo de A usando un puntero o referencia a B2, se necesita algo más que aplicar un desplazamiento. Debemos leer el campo "puntero a A" de B2, seguirlo y solo luego aplicar un desplazamiento, porque dependiendo de qué clase B2 es una base, ese puntero tendrá valores diferentes. No existe tal cosa como offsetof(B2,field of A) : no puede haber. offsetof nunca funcionará con herencia virtual en ninguna implementación.


Para la definición de la estructura de datos POD, aquí tienes la explicación [ya publicada en otra publicación en ]

¿Qué son los tipos de POD en C ++?

Ahora, llegando a su código, está funcionando bien como se esperaba. Esto se debe a que está intentando encontrar el offsetof (), para los miembros públicos de su clase, que es válido.

Por favor, hágamelo saber, la pregunta correcta, si mi punto de vista anterior, no aclara su duda.


Respuesta corta: offsetof es una característica que está solo en el estándar de C ++ para la compatibilidad de C heredada. Por lo tanto, está básicamente restringido a lo que se puede hacer en C. C ++ solo admite lo que debe para compatibilidad con C.

Como offsetof es básicamente un hack (implementado como macro) que se basa en el modelo de memoria simple que soporta C, se necesitaría mucha libertad de los implementadores de compiladores de C ++ para organizar el diseño de la instancia de clase.

El efecto es que offsetof funcionará a menudo (dependiendo del código fuente y del compilador utilizado) en C ++ incluso cuando no esté respaldado por el estándar, excepto cuando no lo haga. Por lo tanto, debe tener mucho cuidado con la compensación de uso en C ++, especialmente porque no conozco un solo compilador que genere una advertencia para uso no POD ...

Editar : como preguntaste por ejemplo, lo siguiente podría aclarar el problema:

#include <iostream> using namespace std; struct A { int a; }; struct B : public virtual A { int b; }; struct C : public virtual A { int c; }; struct D : public B, public C { int d; }; #define offset_d(i,f) (long(&(i)->f) - long(i)) #define offset_s(t,f) offset_d((t*)1000, f) #define dyn(inst,field) {/ cout << "Dynamic offset of " #field " in " #inst ": "; / cout << offset_d(&i##inst, field) << endl; } #define stat(type,field) {/ cout << "Static offset of " #field " in " #type ": "; / cout.flush(); / cout << offset_s(type, field) << endl; } int main() { A iA; B iB; C iC; D iD; dyn(A, a); dyn(B, a); dyn(C, a); dyn(D, a); stat(A, a); stat(B, a); stat(C, a); stat(D, a); return 0; }

Esto se bloqueará al tratar de localizar el campo a dentro del tipo B estáticamente, mientras funciona cuando hay una instancia disponible. Esto se debe a la herencia virtual, donde la ubicación de la clase base se almacena en una tabla de búsqueda.

Si bien este es un ejemplo artificial, una implementación podría usar una tabla de búsqueda también para buscar las secciones pública, protegida y privada de una instancia de clase. O haga que la búsqueda sea completamente dinámica (use una tabla hash para los campos), etc.

El estándar simplemente deja abiertas todas las posibilidades al restringir offsetof a POD (IOW: no hay forma de usar una tabla hash para las estructuras POD ... :)

Solo otra nota: tuve que volver a implementar offsetof (aquí: offset_s) para este ejemplo ya que GCC realmente se equivoca cuando llamo a offsetof para un campo de una clase base virtual.


Si agrega, por ejemplo, un destructor vacío virtual:

virtual ~Foo() {}

Su clase se convertirá en "polimórfica", es decir, tendrá un campo de miembro oculto que es un puntero a un "vtable" que contiene punteros a funciones virtuales.

Debido al campo miembro oculto, el tamaño de un objeto y el desplazamiento de los miembros no serán triviales. Por lo tanto, deberías tener problemas al usar offsetof.