Gerir C ++ objetos em um tampão, tendo em conta as premissas de alinhamento e de memória de layout

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

Pergunta

Eu sou armazenar objetos em um buffer. Agora eu sei que eu não posso fazer suposições sobre o layout de memória do objeto.

Se eu sei o tamanho total do objeto, é acceptible para criar um ponteiro para esta memória e funções de chamadas sobre ele?

por exemplo. Digamos que eu tenha a seguinte classe:

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

1) se eu sei que esta classe para ser de tamanho 24 e eu sei o endereço de onde ele começa na memória enquanto não é seguro assumir o layout da memória é acceptible para converter este para um ponteiro e chamadas funções neste objeto que aceder a estes membros? (Does C ++ sei por alguma mágica a posição correta de um membro?)

2) Se isto não é seguro / ok, há alguma outra maneira que não seja usando um construtor que leva todos os argumentos e puxando cada argumento fora de um tampão de cada vez?

Edit:. Changed título para torná-lo mais adequado para o que eu estou pedindo

Foi útil?

Solução

Você pode criar um construtor que recebe todos os membros e atribui-lhes, em seguida, usar a colocação de novo.

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);

Isto tem a vantagem de que mesmo v-tabela será gerada corretamente. Note, no entanto, se você estiver usando este para serialização, o ponteiro short int sem sinal não vai apontar para algo útil quando você desserializar-lo, a menos que esteja muito cuidado para usar algum tipo de método para ponteiros converter em compensações e depois volta novamente .

Os métodos individuais em um ponteiro this são estaticamente ligados e são simplesmente uma chamada direta para a função com this ser o primeiro parâmetro antes os parâmetros explícitos.

Variáveis ??membro são referenciados usando um deslocamento do ponteiro this. Se um objeto é colocado para fora como este:

0: vtable
4: a
8: b
12: c
etc...

a será acessado por dereferencing this + 4 bytes.

Outras dicas

Basicamente o que você está propondo fazer é ler em um monte de (espero que não aleatório) bytes, lançando-os para um objeto conhecido, e em seguida, chamar um método de classe nesse objeto. Ele pode realmente funcionar, porque os bytes vão acabar no ponteiro "este" em que método de classe. Mas você está tendo uma chance real sobre coisas que não são onde o código compilado espera que ele seja. E ao contrário de Java ou C #, não há "tempo de execução" real para pegar esses tipos de problemas, então o melhor que você vai ter um dump de memória, e na pior das hipóteses você terá de memória corrompido.

Parece que você quer uma versão C ++ de serialização / desserialização de Java. Há provavelmente uma biblioteca lá fora para fazer isso.

chamadas de funções não-virtuais são ligados diretamente como uma função C. O (este) ponteiro objecto é transmitido como o primeiro argumento. Nenhum conhecimento do layout do objeto é necessária para chamar a função.

Parece que você não está armazenando os próprios objetos em um buffer, mas sim os dados a partir do qual eles estão compostas.

Se este dado está na memória na ordem os campos são definidos dentro da sua classe (com preenchimento adequado para a plataforma) e o seu tipo é um POD , então você pode memcpy os dados do buffer para um ponteiro para o seu tipo (ou possivelmente lançá-lo, mas cuidado, há algumas armadilhas específicas da plataforma com elencos de ponteiros de tipos diferentes).

Se sua classe não é um POD, em seguida, o layout em memória de campos não é garantida, e você não deve confiar em qualquer ordenação observada, como é permitida a mudança em cada recompilação.

Você pode, no entanto, iniciar um não-POD com dados de um POD.

Tanto quanto os endereços onde as funções não-virtuais estão localizados: eles são estaticamente ligado em tempo de compilação para algum local dentro do seu segmento de código que é o mesmo para cada instância do seu tipo. Note-se que não há "tempo de execução" envolvidos. Quando você escreve 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;
}

o compilador gera código que faz algo parecido com isto:

  1. função main:
    1. alocar 8 bytes na pilha para o objeto "f"
    2. padrão chamada initializer para a classe "Foo" (não faz nada neste caso)
    3. valor do argumento impulso 42 na pilha
    4. ponteiro de empurrar para objeto "f" na pilha
    5. Faça chamada para a função Foo_i_DoSomething@4 (nome real é geralmente mais complexo)
    6. valor de retorno de carga 0 no registrador acumulador
    7. retorno ao chamador
  2. Foo_i_DoSomething@4 função (localizado em outro lugar no segmento de código)
    1. load valor "x" de pilha (empurrado pelo chamador)
    2. multiplique por 2
    3. load ponteiro "this" de pilha (empurrado pelo chamador)
    4. deslocamento de campo "a" dentro de um objeto Foo calcular
    5. Adicione calculada para compensar ponteiro this, carregado na etapa 3
    6. produto de loja, calculado no passo 2, a compensação calculada no passo 5
    7. load valor "x" de pilha, novamente
    8. load "this" ponteiro da pilha, novamente
    9. deslocamento de campo "a" dentro de um objeto Foo calcular, novamente
    10. Adicione calculada para compensar ponteiro this, carregado no passo 8
    11. load valor "a" armazenado no deslocamento,
    12. agregar valor "a", passo int carregado 12, para o valor "x" carregado no passo 7
    13. load "this" ponteiro da pilha, novamente
    14. deslocamento de campo "b" dentro de um objeto Foo calcular
    15. Adicione calculada para compensar ponteiro this, carregado na etapa 14
    16. sum loja, calculado na etapa 13, a compensação calculada na etapa 16
    17. retorno ao chamador

Em outras palavras, seria mais ou menos o mesmo código como se você tivesse escrito isto (especificidades, como o nome da função DoSomething e método de passagem ponteiro this são até o 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. Um objeto tendo tipo POD, neste caso, já está criado (Querendo ou não você chamar de novo. Alocar o armazenamento necessário já é suficiente), e você pode acessar os membros dele, incluindo chamar uma função em que objeto. Mas isso só irá funcionar se você sabe exatamente o alinhamento necessário de T, e do tamanho do T (o buffer não pode ser menor do que), e o alinhamento de todos os membros do T. Mesmo para um tipo vagem, o compilador é permitido colocar bytes de preenchimento entre os membros, se quiser. Por tipos não-POD, você pode ter a mesma sorte se o seu tipo não tem funções virtuais ou classes de base, nenhum usuário construtor definido (é claro) e que se aplica à base e todos os seus membros não-estáticos também.

  2. Para todos os outros tipos, todas as apostas estão fora. Você tem que ler valores em primeiro lugar com um POD, e depois inicializar um tipo não-POD com esses dados.

Eu sou armazenar objetos em um buffer. ... Se eu sei o tamanho total do objeto, é aceitável para criar um ponteiro para esta memória e funções de chamadas sobre ele?

Isto é aceitável na medida em que o uso de moldes é aceitável:

#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';
}

Fundição um objeto para algo como memória bruta e de volta é realmente muito comum, especialmente no mundo do C. Se você estiver usando uma hierarquia de classes, porém, não faria mais sentido ponteiro uso de funções de membro.

dizer que tenho a seguinte classe: ...

Se eu sei que esta classe para ser de tamanho 24 e eu sei o endereço de onde ele começa na memória ...

Este é o lugar onde as coisas ficam difíceis. O tamanho de um objecto inclui o tamanho dos seus membros de dados (e todos os membros de dados a partir de qualquer base de classes) mais qualquer preenchimento mais quaisquer ponteiros de função ou informação dependente da aplicação, algo menos salvo de certas optimizações tamanho (optimização classe base vazia). Se o número resultante é 0 bytes, em seguida, o objecto é obrigado a tomar, pelo menos, um byte de memória. Essas coisas são uma combinação de questões de linguagem e requisitos comuns que a maioria das CPUs tem sobre acessos à memória. tentando fazer as coisas para funcionar corretamente pode ser uma verdadeira dor .

Se você apenas alocar um objeto e elenco para e da memória matéria você pode ignorar estas questões. Mas se você copiar internos de um objeto para um buffer de algum tipo, então eles traseira de sua cabeça muito rapidamente. O código acima depende de algumas regras gerais sobre o alinhamento (isto é, acontece que eu sei que classe A terão as mesmas restrições de alinhamento como ints, e, portanto, a matriz pode ser convertido com segurança para um A, mas eu não poderia necessariamente garantir a mesmo se eu estivesse fundição de peças da matriz para de um e peças para outras classes com outros membros de dados).

Oh, e quando copiar objetos que você precisa ter certeza de que você está lidando adequadamente ponteiros.

Você pode também estar interessado em coisas como do Google Protocol Buffers ou o Facebook Thrift .


Sim estas questões são difíceis. E, sim, algumas linguagens de programação varrê-los para debaixo do tapete. Mas há uma enorme quantidade de coisas ficando varrido para debaixo do tapete :

HotSpot JVM Em Sun, armazenamento objeto é alinhado com o limite de 64-bit mais próximo. Além de tudo isso, cada objeto tem um cabeçalho de 2 palavra na memória. tamanho da palavra do JVM é geralmente o tamanho ponteiro nativa da plataforma. (Um objecto que consiste em apenas uma de 32 bits int e um de 64-bit duplo - 96 bits de dados - exigirá) duas palavras para o objecto de cabeçalho, uma palavra para o int, duas palavras para o dobro. Isso é 5 palavras: 160 bits. Devido ao alinhamento, este objeto vai ocupar 192 bits de memória.

Isto é porque a Sun se baseia numa tática relativamente simples para problemas de alinhamento de memória (em um processador imaginário, um carvão animal pode ser permitido existir em qualquer posição de memória, um int em qualquer local que é divisível por quatro, e um duplo pode precisar ser alocados apenas em locais de memória que são divisível por 32 -. mas o requisito mais alinhamento restritiva também satisfaz todos os outros requisitos de alinhamento, de modo Sun é alinhar tudo de acordo com a localização mais restritiva)

Outra tática para o alinhamento de memória pode recuperar algum desse espaço .

  1. Se a classe não contém funções virtuais (e, portanto, instâncias de classe não têm vptr), e se você fizer correta suposições sobre a maneira pela qual a classe de dados membro é colocado para fora na memória, em seguida, fazendo o que você está sugerindo trabalho poder (mas pode não ser portátil).
  2. Sim, uma outra maneira (mais idiomática, mas não muito mais seguro ... você ainda precisa saber como a classe expõe seus dados) seria usar o chamado "operador de posicionamento novo" e um construtor padrão.

Isso depende do que você entende por "seguro". Toda vez que você lançar um endereço de memória em um ponto desta forma você está ignorando as características tipo de segurança fornecidas pelo compilador, e assumir a responsabilidade para si mesmo. Se, como Chris implica, você faz uma suposição incorreta sobre o layout de memória, ou detalhes de implementação do compilador, então você vai obter resultados inesperados e portabilidade solto.

Uma vez que você está preocupado com a "segurança" deste estilo de programação vale a pena provavelmente o seu tempo a investigar métodos portáteis e tipo seguro como pré-existente bibliotecas, ou escrever um operador construtor ou cessão para o efeito.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top