Administrar objetos C ++ en un búfer, considerando los supuestos de alineación y diseño de memoria

StackOverflow https://stackoverflow.com/questions/417446

Pregunta

Estoy almacenando objetos en un búfer. Ahora sé que no puedo hacer suposiciones sobre el diseño de la memoria del objeto.

Si conozco el tamaño general del objeto, ¿es aceptable crear un puntero a esta memoria y ejecutar funciones en él?

por ejemplo digamos que tengo la siguiente clase:

[int,int,int,int,char,padding*3bytes,unsigned short int*]

1) si sé que esta clase es de tamaño 24 y sé la dirección de donde comienza en la memoria si bien no es seguro asumir que el diseño de la memoria es aceptable lanzar esto a un puntero y llamar a funciones en este objeto que acceden a estos miembros? (¿C ++ sabe por magia la posición correcta de un miembro?)

2) Si esto no es seguro / está bien, ¿hay alguna otra forma que no sea usar un constructor que tome todos los argumentos y saque cada argumento del búfer uno por uno?

Editar: se cambió el título para que sea más apropiado para lo que estoy preguntando.

¿Fue útil?

Solución

Puede crear un constructor que tome todos los miembros y los asigne, luego use la ubicación nueva.

class Foo
{
    int a;int b;int c;int d;char e;unsigned short int*f;
public:
    Foo(int A,int B,int C,int D,char E,unsigned short int*F) : a(A), b(B), c(C), d(D), e(E), f(F) {}
};

...
char *buf  = new char[sizeof(Foo)];   //pre-allocated buffer
Foo *f = new (buf) Foo(a,b,c,d,e,f);

Esto tiene la ventaja de que incluso la v-table se generará correctamente. Sin embargo, tenga en cuenta que si está utilizando esto para la serialización, el puntero int corto sin signo no apuntará a nada útil cuando lo deserialice, a menos que tenga mucho cuidado de usar algún tipo de método para convertir punteros en compensaciones y luego volver .

Los métodos individuales en un puntero this están vinculados estáticamente y son simplemente una llamada directa a la función, siendo this el primer parámetro antes de los parámetros explícitos.

Se hace referencia a las

variables miembro utilizando un desplazamiento desde el puntero this . Si un objeto se presenta así:

0: vtable
4: a
8: b
12: c
etc...
Se accederá a

a haciendo referencia a this + 4 bytes .

Otros consejos

Básicamente, lo que propone hacer es leer en un montón de bytes (con suerte no aleatorios), convertirlos en un objeto conocido y luego llamar a un método de clase en ese objeto. En realidad, podría funcionar, porque esos bytes terminarán en el " this " puntero en ese método de clase. Pero está tomando una oportunidad real de que las cosas no estén donde el código compilado espera que esté. Y a diferencia de Java o C #, no existe un "tiempo de ejecución" real para detectar este tipo de problemas, por lo que, en el mejor de los casos, obtendrá un volcado del núcleo y, en el peor, obtendrá una memoria dañada.

Parece que quiere una versión C ++ de la serialización / deserialización de Java. Probablemente hay una biblioteca por ahí para hacer eso.

Las llamadas a funciones no virtuales están vinculadas directamente al igual que una función C. El puntero del objeto (este) se pasa como primer argumento. No se requiere conocimiento del diseño del objeto para llamar a la función.

Parece que no está almacenando los objetos en un búfer, sino los datos de los que están compuestos.

Si estos datos están en la memoria en el orden en que se definen los campos dentro de su clase (con el relleno adecuado para la plataforma) y su tipo es POD , luego puede memcpy los datos del almacenarse en un puntero a su tipo (o posiblemente lanzarlo, pero tenga cuidado, hay algunas trampas específicas de la plataforma con lanzamientos a punteros de diferentes tipos).

Si su clase no es un POD, el diseño de los campos en la memoria no está garantizado, y no debe confiar en ningún orden observado, ya que está permitido cambiar en cada compilación.

Sin embargo, puede inicializar un no POD con datos de un POD.

En cuanto a las direcciones donde se encuentran las funciones no virtuales: están vinculadas estáticamente en el momento de la compilación a alguna ubicación dentro de su segmento de código que es la misma para cada instancia de su tipo. Tenga en cuenta que no hay "tiempo de ejecución" involucrado. Cuando escribes código como este:

class Foo{
   int a;
   int b;

public:
   void DoSomething(int x);
};

void Foo::DoSomething(int x){a = x * 2; b = x + a;}

int main(){
    Foo f;
    f.DoSomething(42);
    return 0;
}

el compilador genera código que hace algo como esto:

  1. función principal :
    1. asigna 8 bytes en la pila para el objeto " f "
    2. inicializador predeterminado de llamada para la clase " Foo " (no hace nada en este caso)
    3. valor de argumento de inserción 42 en la pila
    4. empujar el puntero al objeto " f " en la pila
    5. hacer una llamada a la función Foo_i_DoSomething @ 4 (el nombre real suele ser más complejo)
    6. cargar el valor de retorno 0 en el registro del acumulador
    7. volver a la persona que llama
  2. function Foo_i_DoSomething @ 4 (ubicado en otra parte del segmento de código)
    1. cargar " x " valor de la pila (empujado por la persona que llama)
    2. multiplicar por 2
    3. cargar " this " puntero desde la pila (empujado por la persona que llama)
    4. calcular el desplazamiento del campo " a " dentro de un objeto Foo
    5. agregue el desplazamiento calculado a este puntero, cargado en el paso 3
    6. almacenar producto, calculado en el paso 2, para compensar calculado en el paso 5
    7. cargar " x " valor de la pila, nuevamente
    8. cargar " this " puntero de la pila, nuevamente
    9. calcular el desplazamiento del campo " a " dentro de un objeto Foo , nuevamente
    10. agregue el desplazamiento calculado a este puntero, cargado en el paso 8
    11. cargar " a " valor almacenado en el desplazamiento,
    12. agregar " a " valor, cargado en el paso 12, a " x " valor cargado en el paso 7
    13. cargar " this " puntero de la pila, nuevamente
    14. calcular el desplazamiento del campo " b " dentro de un objeto Foo
    15. agregue el desplazamiento calculado a este puntero, cargado en el paso 14
    16. suma de tienda, calculada en el paso 13, para compensar calculada en el paso 16
    17. volver a la persona que llama

En otras palabras, sería más o menos el mismo código que si hubiera escrito esto (los detalles, como el nombre de la función DoSomething y el método para pasar el puntero this dependen del compilador) :

class Foo{
    int a;
    int b;

    friend void Foo_DoSomething(Foo *f, int x);
};

void Foo_DoSomething(Foo *f, int x){
    f->a = x * 2;
    f->b = x + f->a;
}

int main(){
    Foo f;
    Foo_DoSomething(&f, 42);
    return 0;
}
  1. Un objeto que tiene tipo POD, en este caso, ya está creado (ya sea que llame nuevo o no. Asignar el almacenamiento requerido ya es suficiente), y puede acceder a los miembros del mismo, incluida la llamada a una función en ese objeto. Pero eso solo funcionará si conoce con precisión la alineación requerida de T y el tamaño de T (el búfer puede no ser menor que él), y la alineación de todos los miembros de T. Incluso para un tipo de pod, el compilador es permitido poner bytes de relleno entre los miembros, si así lo desea. Para los tipos que no son POD, puede tener la misma suerte si su tipo no tiene funciones virtuales o clases base, ningún constructor definido por el usuario (por supuesto) y eso se aplica a la base y a todos sus miembros no estáticos también.

  2. Para todos los demás tipos, todas las apuestas están desactivadas. Primero tiene que leer los valores con un POD y luego inicializar un tipo que no sea POD con esos datos.

  

Estoy almacenando objetos en un búfer. ... Si conozco el tamaño general del objeto, ¿es aceptable crear un puntero a esta memoria y ejecutar funciones en él?

Esto es aceptable en la medida en que el uso de yesos es aceptable:

#include <iostream>

namespace {
    class A {
        int i;
        int j;
    public:
        int value()
        {
            return i + j;
        }
    };
}

int main()
{
    char buffer[] = { 1, 2 };
    std::cout << reinterpret_cast<A*>(buffer)->value() << '\n';
}

Lanzar un objeto a algo como memoria en bruto y viceversa es bastante común, especialmente en el mundo C. Sin embargo, si está utilizando una jerarquía de clases, tendría más sentido utilizar las funciones de puntero a miembro.

  

di que tengo la siguiente clase: ...

     

si sé que esta clase es de tamaño 24 y sé la dirección de donde comienza en la memoria ...

Aquí es donde las cosas se ponen difíciles. El tamaño de un objeto incluye el tamaño de sus miembros de datos (y cualquier miembro de datos de cualquier clase base) más cualquier relleno más cualquier puntero de función o información dependiente de la implementación, menos todo lo guardado de ciertas optimizaciones de tamaño (optimización de clase base vacía). Si el número resultante es 0 bytes, entonces se requiere que el objeto tome al menos un byte en la memoria. Estas cosas son una combinación de problemas de idioma y requisitos comunes que la mayoría de las CPU tienen con respecto a los accesos a la memoria. Intentar que las cosas funcionen correctamente puede ser un verdadero dolor .

Si solo asigna un objeto y emite desde y hacia la memoria sin procesar, puede ignorar estos problemas. Pero si copia las partes internas de un objeto a un búfer de algún tipo, entonces levantan la cabeza con bastante rapidez. El código anterior se basa en algunas reglas generales sobre la alineación (es decir, sé que la clase A tendrá las mismas restricciones de alineación que los ints, y por lo tanto la matriz se puede convertir de forma segura en una A; pero no necesariamente podría garantizar el lo mismo si estuviera convirtiendo partes de la matriz en A y partes en otras clases con otros miembros de datos).

Ah, y al copiar objetos necesita asegurarse de que está manejando correctamente los punteros.

También puede estar interesado en cosas como Búfers de protocolo de Google o Ahorro de Facebook .


Sí, estos problemas son difíciles. Y sí, algunos lenguajes de programación los barren debajo de la alfombra. Pero hay muchísimas cosas obteniendo barrido debajo de la alfombra :

  

En la JVM HotSpot de Sun, el almacenamiento de objetos está alineado con el límite de 64 bits más cercano. Además de esto, cada objeto tiene un encabezado de 2 palabras en la memoria. El tamaño de palabra de la JVM suele ser el tamaño del puntero nativo de la plataforma. (Un objeto que consta de solo un int de 32 bits y un doble de 64 bits - 96 bits de datos - requerirá) dos palabras para el encabezado del objeto, una palabra para el int, dos palabras para el doble. Eso son 5 palabras: 160 bits. Debido a la alineación, este objeto ocupará 192 bits de memoria.

Esto se debe a que Sun confía en una táctica relativamente simple para problemas de alineación de memoria (en un procesador imaginario, se puede permitir que exista un carácter en cualquier ubicación de memoria, un int en cualquier ubicación que sea divisible por 4, y un doble Es posible que deba asignarse solo en ubicaciones de memoria que son divisibles por 32, pero el requisito de alineación más restrictivo también satisface cualquier otro requisito de alineación, por lo que Sun está alineando todo de acuerdo con la ubicación más restrictiva).

Otra táctica para la alineación de la memoria puede recuperar parte de ese espacio .

  1. Si la clase no contiene funciones virtuales (y, por lo tanto, las instancias de la clase no tienen vptr), y si realiza suposiciones correctas sobre la forma en que se almacenan los datos de los miembros de la clase en la memoria, entonces hacer lo que está sugiriendo podría funcionar (pero podría no ser portátil).
  2. Sí, otra forma (más idiomática pero no mucho más segura ... todavía necesita saber cómo la clase presenta sus datos) sería utilizar el llamado "operador de colocación nuevo". y un constructor predeterminado.

Eso depende de lo que quiera decir con "seguro". Cada vez que convierte una dirección de memoria en un punto de esta manera, omite las características de seguridad de tipo proporcionadas por el compilador y asume la responsabilidad. Si, como Chris implica, hace una suposición incorrecta sobre el diseño de la memoria o los detalles de implementación del compilador, obtendrá resultados inesperados y pérdida de portabilidad.

Dado que le preocupa la "seguridad" de este estilo de programación, probablemente valga la pena investigar métodos portátiles y seguros de tipo, como bibliotecas preexistentes, o escribir un constructor u operador de asignación para ese propósito.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top