Gestione degli oggetti C ++ in un buffer, considerando i presupposti di allineamento e layout di memoria

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

Domanda

Sto memorizzando oggetti in un buffer. Ora so che non posso fare ipotesi sul layout di memoria dell'oggetto.

Se conosco la dimensione complessiva dell'oggetto, è accettabile creare un puntatore a questa memoria e chiamare funzioni su di essa?

es. dire che ho la seguente classe:

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

1) se conosco questa classe della dimensione 24 e conosco l'indirizzo di dove inizia in memoria mentre non è sicuro supporre che il layout di memoria sia accettabile per lanciarlo su un puntatore e chiamare funzioni su questo oggetto che accedono a questi membri? (C ++ conosce per magia la posizione corretta di un membro?)

2) Se ciò non è sicuro / ok, esiste un altro modo se non quello di usare un costruttore che prende tutti gli argomenti e estrae ogni argomento dal buffer uno alla volta?

Modifica: titolo modificato per renderlo più appropriato a ciò che sto chiedendo.

È stato utile?

Soluzione

Puoi creare un costruttore che prende tutti i membri e li assegna, quindi usa il posizionamento nuovo.

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

Questo ha il vantaggio che anche la v-table verrà generata correttamente. Si noti, tuttavia, se lo si utilizza per la serializzazione, il puntatore int corto senza segno non indicherà nulla di utile quando si deserializza, a meno che non si sia molto attenti a utilizzare una sorta di metodo per convertire i puntatori in offset e poi di nuovo indietro .

I singoli metodi su un puntatore this sono collegati staticamente e sono semplicemente una chiamata diretta alla funzione con this come primo parametro prima dei parametri espliciti.

Le variabili membro vengono referenziate usando un offset dal puntatore this . Se un oggetto è disposto in questo modo:

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

a si accederà dereferenziando questo + 4 byte .

Altri suggerimenti

Fondamentalmente quello che stai proponendo di fare è leggere in un mucchio di byte (speriamo non casuali), lanciarli su un oggetto noto e quindi chiamare un metodo di classe su quell'oggetto. Potrebbe effettivamente funzionare, perché quei byte finiranno nel " questo " puntatore in quel metodo di classe. Ma stai correndo una vera possibilità su cose che non si trovano dove il codice compilato si aspetta che sia. E a differenza di Java o C #, non esiste un vero "runtime" per rilevare questo tipo di problemi, quindi nella migliore delle ipotesi si otterrà un dump principale e, nel peggiore dei casi, si avrà la memoria danneggiata.

Sembra che tu voglia una versione C ++ della serializzazione / deserializzazione di Java. Probabilmente c'è una biblioteca là fuori per farlo.

Le chiamate di funzione non virtuali sono collegate direttamente come una funzione C. Il puntatore oggetto (questo) viene passato come primo argomento. Non è richiesta alcuna conoscenza del layout dell'oggetto per chiamare la funzione.

Sembra che non si stiano memorizzando gli oggetti stessi in un buffer, ma piuttosto i dati da cui sono composti.

Se questi dati sono in memoria nell'ordine in cui i campi sono definiti all'interno della tua classe (con il corretto riempimento per la piattaforma) e il tuo tipo è POD , quindi puoi memcpy i dati dal buffer a un puntatore al tuo tipo (o eventualmente lanciarlo, ma attenzione, ci sono alcuni gotcha specifici della piattaforma con cast a puntatori di diversi tipi).

Se la tua classe non è un POD, il layout dei campi in memoria non è garantito e non dovresti fare affidamento su alcun ordine osservato, poiché è consentito cambiare su ogni ricompilazione.

Tuttavia, è possibile inizializzare un non-POD con i dati di un POD.

Per quanto riguarda gli indirizzi in cui si trovano le funzioni non virtuali: sono collegate staticamente in fase di compilazione a una posizione all'interno del segmento di codice che è la stessa per ogni istanza del tuo tipo. Nota che non esiste "runtime" coinvolti. Quando scrivi codice in questo modo:

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

il compilatore genera codice che fa qualcosa del genere:

  1. funzione main :
    1. alloca 8 byte in pila per l'oggetto " f "
    2. chiama l'inizializzatore predefinito per la classe " Foo " (non fa nulla in questo caso)
    3. inserisce il valore argomento 42 nello stack
    4. premi il puntatore sull'oggetto " f " nello stack
    5. chiama alla funzione Foo_i_DoSomething @ 4 (il nome reale è di solito più complesso)
    6. carica il valore di ritorno 0 nel registro dell'accumulatore
    7. torna al chiamante
  2. funzione Foo_i_DoSomething @ 4 (situato altrove nel segmento di codice)
    1. carica " x " valore dallo stack (attivato dal chiamante)
    2. moltiplica per 2
    3. carica " questo " puntatore dallo stack (attivato dal chiamante)
    4. calcola offset del campo " a " all'interno di un oggetto Foo
    5. aggiungi l'offset calcolato a questo puntatore, caricato nel passaggio 3
    6. negozio prodotto, calcolato nel passaggio 2, per compensare calcolato nel passaggio 5
    7. carica " x " valore dallo stack, di nuovo
    8. carica " questo " puntatore dallo stack, di nuovo
    9. calcola offset del campo " a " all'interno di un oggetto Foo , di nuovo
    10. aggiungi l'offset calcolato a questo puntatore, caricato nel passaggio 8
    11. carica " a " valore memorizzato all'offset,
    12. aggiungi " a " valore, caricato nel passaggio 12, in " x " valore caricato nel passaggio 7
    13. carica " questo " puntatore dallo stack, di nuovo
    14. calcola offset del campo " b " all'interno di un oggetto Foo
    15. aggiungi l'offset calcolato a questo puntatore, caricato nel passaggio 14
    16. somma negozio, calcolata nel passaggio 13, per compensare calcolata nel passaggio 16
    17. torna al chiamante

In altre parole, sarebbe più o meno lo stesso codice come se lo avessi scritto (dettagli, come il nome della funzione DoSomething e il metodo per passare il puntatore this sono al compilatore) :

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 oggetto con tipo POD, in questo caso, è già stato creato (indipendentemente dal fatto che tu chiami nuovo. L'allocazione della memoria richiesta è già sufficiente) e puoi accedere ai membri di esso, inclusa la chiamata a una funzione su quella oggetto. Ma funzionerà solo se si conosce con precisione l'allineamento richiesto di T e la dimensione di T (il buffer potrebbe non essere più piccolo di esso) e l'allineamento di tutti i membri di T. Anche per un tipo di pod, il compilatore è permesso di inserire byte di riempimento tra i membri, se lo desidera. Per i tipi non POD, puoi avere la stessa fortuna se il tuo tipo non ha funzioni virtuali o classi base, nessun costruttore definito dall'utente (ovviamente) e questo vale anche per la base e tutti i suoi membri non statici.

  2. Per tutti gli altri tipi, tutte le scommesse sono disattivate. Devi prima leggere i valori con un POD, quindi inizializzare un tipo non POD con quei dati.

  

Sto memorizzando oggetti in un buffer. ... Se conosco la dimensione complessiva dell'oggetto, è accettabile creare un puntatore a questa memoria e chiamare funzioni su di essa?

Questo è accettabile nella misura in cui l'uso dei cast è accettabile:

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

Trasmettere un oggetto a qualcosa come la memoria grezza e viceversa è in realtà abbastanza comune, specialmente nel mondo C. Se si utilizza una gerarchia di classi, tuttavia, sarebbe più sensato utilizzare il puntatore alle funzioni membro.

  

dire che ho la seguente classe: ...

     

se conosco questa classe della dimensione 24 e conosco l'indirizzo di dove inizia in memoria ...

Qui le cose si fanno difficili. La dimensione di un oggetto include la dimensione dei suoi membri di dati (e di tutti i membri di dati di qualsiasi classe di base) più qualsiasi riempimento, più eventuali puntatori di funzione o informazioni dipendenti dall'implementazione, meno tutto ciò che viene salvato da determinate ottimizzazioni di dimensione (ottimizzazione della classe di base vuota). Se il numero risultante è 0 byte, l'oggetto deve occupare almeno un byte in memoria. Queste cose sono una combinazione di problemi di lingua e requisiti comuni che la maggior parte delle CPU ha riguardo agli accessi alla memoria. Cercare di far funzionare correttamente le cose può essere una vera seccatura .

Se si assegna un oggetto e si esegue il cast da e verso la memoria non elaborata, è possibile ignorare questi problemi. Ma se copi gli interni di un oggetto in un buffer di qualche tipo, allora alzano la testa abbastanza rapidamente. Il codice sopra si basa su alcune regole generali sull'allineamento (ad esempio, mi capita di sapere che la classe A avrà le stesse restrizioni di allineamento di ints e quindi l'array può essere castato in modo sicuro su una A; ma non potrei necessariamente garantire che lo stesso se stavo lanciando parti dell'array su A e parti su altre classi con altri membri di dati).

Oh, e quando copi gli oggetti devi assicurarti di gestire correttamente i puntatori.

Potresti anche essere interessato a cose come Buffer di protocollo di Google o Facebook's Thrift .


Sì, questi problemi sono difficili. E, sì, alcuni linguaggi di programmazione li spazzano sotto il tappeto. Ma c'è un sacco di roba da ottenere spazzato sotto il tappeto :

  

In HotSpot JVM di Sun, l'archiviazione degli oggetti è allineata al limite più vicino a 64 bit. Inoltre, ogni oggetto ha un'intestazione di 2 parole in memoria. La dimensione della parola della JVM è generalmente la dimensione del puntatore nativo della piattaforma. (Un oggetto composto solo da un int a 32 bit e un doppio a 64 bit - 96 bit di dati - richiederà due parole per l'intestazione dell'oggetto, una parola per l'int, due parole per il doppio. Sono 5 parole: 160 bit. A causa dell'allineamento, questo oggetto occuperà 192 bit di memoria.

Questo perché Sun si affida a una tattica relativamente semplice per i problemi di allineamento della memoria (su un processore immaginario, un carattere può essere autorizzato a esistere in qualsiasi posizione di memoria, un int in qualsiasi posizione che è divisibile per 4 e un doppio potrebbe essere necessario allocarlo solo in posizioni di memoria divisibili per 32, ma il requisito di allineamento più restrittivo soddisfa anche ogni altro requisito di allineamento, quindi Sun sta allineando tutto in base alla posizione più restrittiva).

Un'altra tattica per l'allineamento della memoria può recuperare parte di quello spazio .

  1. Se la classe non contiene funzioni virtuali (e quindi le istanze della classe non hanno vptr) e se si fanno corretti ipotesi sul modo in cui i dati dei membri della classe sono disposti in memoria, allora fare ciò che stai suggerendo potrebbe funzionare (ma potrebbe non essere portatile).
  2. Sì, un altro modo (più idiomatico ma non molto più sicuro ... devi ancora sapere come la classe espone i suoi dati) sarebbe quello di utilizzare il cosiddetto operatore di posizionamento "nuovo" e un costruttore predefinito.

Dipende da cosa intendi per "sicuro". Ogni volta che si inserisce un indirizzo di memoria in un punto in questo modo si ignora le funzionalità di sicurezza del tipo fornite dal compilatore e si assume la responsabilità. Se, come suggerisce Chris, fai un'ipotesi errata sul layout della memoria o sui dettagli dell'implementazione del compilatore, otterrai risultati imprevisti e portabilità sciolta.

Dato che sei preoccupato per la "sicurezza" di questo stile di programmazione vale probabilmente la pena esaminare i metodi portatili e sicuri per il tipo come librerie preesistenti o scrivere un costruttore o un operatore di assegnazione allo scopo.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top