Visualizzazione dei risultati da 1 a 7 su 7
  1. #1

    [C++]-Ereditarietà multipla e classi virtuali

    Salve a tutti!
    Vorrei un chiarimento riguardante la seguente situazione: consideriamo la seguente gerarchia tra classi
    codice:
    class persona
    {
         protected:
              int p;
    };
    
    class studente:virtual public persona
    {
         protected:
              int s;
    };
    
    class lavoratore:virtual public persona
    {
         protected:
              int l;
    };
    
    class studente_lavoratore: public studente, public lavoratore
    {
         private:
              int sl;
    };
    In tale situazione, ereditando la classe base "persona" utilizzando la parola "virtual", si evita che nella classe studente_lavoratore ci siano due copie della variabile int p.(Cosa che accadrebbe se si togliesse la parola virtual).

    Detto ciò, se si prova a fare una sizeof degli oggetti di queste quattro classi, la situazione è la seguente:
    -sizeof(persona) 4 byte----> int p 4 byte
    -sizeof(studente) 12 byte---->int s 4 byte e int p 4 byte
    -sizeof(lavoratore) 12 byte--->int l 4 byte e int p 4 byte
    -sizeof(studente_lavoratore) 24 byte--->int sl 4 byte, int s 4 byte, int l 4 byte e int p 4 byte


    Per un oggetto di classe "studente", perchè la sizeof restituisce 12 byte? Cosa viene aggiunto?
    Stesso dicasi per la classe "lavoratore" e per la classe "studente_lavoratore". Cosa viene aggiunto?

    Grazie.
    Ultima modifica di MItaly; 11-02-2016 a 03:59

  2. #2
    Per un oggetto di classe "studente", perchè la sizeof restituisce 12 byte? Cosa viene aggiunto?
    Stesso dicasi per la classe "lavoratore" e per la classe "studente_lavoratore". Cosa viene aggiunto?
    Un puntatore al subobject della classe base.
    Prendiamo studente:
    codice:
    class studente:virtual public persona
    {
         protected:
              int s;
    };
    il suo layout di fatto sarà equivalente a qualcosa di questo tipo:
    codice:
    struct studente
    {
        persona *_studente_baseptr;
        int s;
        persona _base;
    }
    con _studente_baseptr che viene inizializzato a &_base al momento della costruzione; lo stesso dicasi per lavoratore.

    Per studente_lavoratore, la questione sarà di questo tipo:
    codice:
    struct studente_lavoratore
    {
        // subobject studente
        persona *_studente_baseptr;
        int s;
        // subobject lavoratore
        persona *_lavoratore_baseptr;
        int l;
        // subobject classe base condivisa
        persona _base;
    };
    Anche qui, i due baseptr vengono inizializzati a _base al momento della costruzione.

    Qual è lo scopo di tutto questo giro di puntatori? Per fare in modo che codice che si aspetta uno studente* possa funzionare correttamente con uno studente_lavoratore, facendo riferimento al subobject di classe base corretto in ogni caso.

    Immaginiamo che il compilatore debba compilare del codice di questo tipo:
    codice:
    void foo(studente *s) {
        s->p = 12;
    }
    
    void bar(lavoratore *l) {
        l->p = 24;
    }
    
    int main() {
        studente s;
        studente_lavoratore sl;
        foo(&s);
        foo(&sl);
        return 0;
    }
    Il codice diventa del tipo:
    codice:
    void foo(studente *s) {
        s->_studente_baseptr->p = 12;
    }
    
    void bar(lavoratore *l) {
        s->_lavoratore_baseptr->p = 12;
    }
    
    int main() {
        studente s;
        s._studente_baseptr = &s._base;
        studente_lavoratore sl;
        s._studente_baseptr = &sl._base;
        foo(&s);
        foo(&sl);
        bar((lavoratore *)&_lavoratore_baseptr);
        return 0;
    }
    Il codice di foo viene compilato una sola volta, indipendentemente dal tipo della classe derivata che può essere passato (in cui la posizione del subobject di classe base può variare), ma dato che tutti i riferimenti alla classe base avvengono in modo indiretto tramite il puntatore il risultato finale è sempre corretto. Nota tra l'altro che per passare uno studente_lavoratore ad una funzione che si aspetta un lavoratore * il compilatore deve aggiustare il puntatore prima del passaggio (deve passare un puntatore al primo membro del subobject di tipo lavoratore), ma a parte questo funzioni che si aspettano un lavoratore * funzionano in maniera trasparente.
    Amaro C++, il gusto pieno dell'undefined behavior.

  3. #3
    Quote Originariamente inviata da MItaly Visualizza il messaggio
    Un puntatore al subobject della classe base.
    Prendiamo studente:
    codice:
    class studente:virtual public persona
    {
         protected:
              int s;
    };
    il suo layout di fatto sarà equivalente a qualcosa di questo tipo:
    codice:
    struct studente
    {
        persona *_studente_baseptr;
        int s;
        persona _base;
    }
    con _studente_baseptr che viene inizializzato a &_base al momento della costruzione; lo stesso dicasi per lavoratore.

    Per studente_lavoratore, la questione sarà di questo tipo:
    codice:
    struct studente_lavoratore
    {
        // subobject studente
        persona *_studente_baseptr;
        int s;
        // subobject lavoratore
        persona *_lavoratore_baseptr;
        int l;
        // subobject classe base condivisa
        persona _base;
    };
    Anche qui, i due baseptr vengono inizializzati a _base al momento della costruzione.

    Qual è lo scopo di tutto questo giro di puntatori? Per fare in modo che codice che si aspetta uno studente* possa funzionare correttamente con uno studente_lavoratore, facendo riferimento al subobject di classe base corretto in ogni caso.

    Immaginiamo che il compilatore debba compilare del codice di questo tipo:
    codice:
    void foo(studente *s) {
        s->p = 12;
    }
    
    void bar(lavoratore *l) {
        l->p = 24;
    }
    
    int main() {
        studente s;
        studente_lavoratore sl;
        foo(&s);
        foo(&sl);
        return 0;
    }
    Il codice diventa del tipo:
    codice:
    void foo(studente *s) {
        s->_studente_baseptr->p = 12;
    }
    
    void bar(lavoratore *l) {
        s->_lavoratore_baseptr->p = 12;
    }
    
    int main() {
        studente s;
        s._studente_baseptr = &s._base;
        studente_lavoratore sl;
        s._studente_baseptr = &sl._base;
        foo(&s);
        foo(&sl);
        bar((lavoratore *)&_lavoratore_baseptr);
        return 0;
    }
    Il codice di foo viene compilato una sola volta, indipendentemente dal tipo della classe derivata che può essere passato (in cui la posizione del subobject di classe base può variare), ma dato che tutti i riferimenti alla classe base avvengono in modo indiretto tramite il puntatore il risultato finale è sempre corretto. Nota tra l'altro che per passare uno studente_lavoratore ad una funzione che si aspetta un lavoratore * il compilatore deve aggiustare il puntatore prima del passaggio (deve passare un puntatore al primo membro del subobject di tipo lavoratore), ma a parte questo funzioni che si aspettano un lavoratore * funzionano in maniera trasparente.
    Ti ringrazio per la risposta.

    Vorrei chiederti un'altra cosa: supponiamo di inserire per tutte e quattro le classi uno specificatore di accesso di tipo "public" (consieriamo questo solo in questo esempio per semplificare il tutto. So che non deve essere fatto poichè andrebbe contro il concetto di "information hiding", che caratterizza la programmazione orientata agli oggetti).

    Come avverrebbe l'accesso alla variabile "int p" presente nella classe studente_lavoratore(poichè è stata ereditata), se nel main scrivessi questa linea di codice?
    codice:
    int main()
    {
          studente_lavoratore SL;
    
          SL.p=10;
    
        
          return 0;
    }
    Considerando che la classe studente_lavoratore avrebbe questa struttura(come hai scritto tu nel messaggio precedente)
    codice:
    struct studente_lavoratore
    {
        // subobject studente
        persona *_studente_baseptr;
        int s;
        // subobject lavoratore
        persona *_lavoratore_baseptr;
        int l;
        // subobject classe base condivisa
        persona _base;
    };
    e che i puntatori _studente_baseptr e _lavoratore_baseptr sono inizializzati con l'indirizzo di _base, la linea SL.p sarebbe equivalente a SL._studente_baseptr->p=10?
    oppure equivalente a SL._lavoratore_baseptr->p=10?
    oppure a qualcos'altro?

    Grazie.
    Ultima modifica di MItaly; 17-02-2016 a 09:53

  4. #4
    Visto che entrambi i puntatori puntano necessariamente allo stesso subobject (è il punto stesso dell'ereditarietà virtual) l'accesso può avvenire tramite uno qualsiasi dei due puntatori, anche se in questo caso specifico il compilatore già sa con esattezza il tipo "vero" dell'oggetto (visto che si tratta di una variabile locale), perciò l'offset di p verrà risolto completamente a compile time, senza passare per i puntatori in questione.
    Amaro C++, il gusto pieno dell'undefined behavior.

  5. #5
    Quote Originariamente inviata da MItaly Visualizza il messaggio
    Visto che entrambi i puntatori puntano necessariamente allo stesso subobject (è il punto stesso dell'ereditarietà virtual) l'accesso può avvenire tramite uno qualsiasi dei due puntatori, anche se in questo caso specifico il compilatore già sa con esattezza il tipo "vero" dell'oggetto (visto che si tratta di una variabile locale), perciò l'offset di p verrà risolto completamente a compile time, senza passare per i puntatori in questione.
    Quindi, la linea di codice SL.p=10 sarebbe equivalente a SL._base.p=10. Giusto?

  6. #6
    Esatto.

    Tra parentesi, guardando l'implementazione effettiva vedo che in realtà non viene memorizzato un puntatore al subobject base, quanto il normale puntatore alla vtable, in cui è memorizzato (oltre all'indirizzo degli eventuali metodi virtuali e al type info) l'offset del subobject base rispetto al "this". Questo è più efficiente, dato che non c'è bisogno di aggiustare il puntatore al momento della copia, e evita di tenere puntatori separati per vtable e subobject base. Il concetto di base comunque resta simile.

    Per ulteriori informazioni sulla questione, qui c'è tutto.
    Amaro C++, il gusto pieno dell'undefined behavior.

  7. #7
    Quote Originariamente inviata da MItaly Visualizza il messaggio
    Esatto.

    Tra parentesi, guardando l'implementazione effettiva vedo che in realtà non viene memorizzato un puntatore al subobject base, quanto il normale puntatore alla vtable, in cui è memorizzato (oltre all'indirizzo degli eventuali metodi virtuali e al type info) l'offset del subobject base rispetto al "this". Questo è più efficiente, dato che non c'è bisogno di aggiustare il puntatore al momento della copia, e evita di tenere puntatori separati per vtable e subobject base. Il concetto di base comunque resta simile.

    Per ulteriori informazioni sulla questione, qui c'è tutto.
    Ho letto la pagina che mi hai consigliato e vorrei chiederti alcune cose.
    Consideriamo questa situazione(sempre in riferimento alla struttura gerarchica di cui stiamo parlando):

    int main()
    {
    persona *q;
    studente *s;
    lavoratore *l;
    studente_lavoratore SL;

    s=&SL;
    s->p=10;

    l=&SL;
    l->p=100;

    q=&SL;
    q->p=1000;

    return 0;

    }

    Con queste linee di codice: s=&SL e s->p=10, l'accesso alla varibile "p" sarebbe di questo tipo:
    s->puntatore_alla_vtable->offset e poi attraverso l'offset che ricavo dalla vtable, so dove saltare per accedere alla varibile "p". Giusto?

    Stesso discorso per le linee di codice: l=&SL e l->p=100. Giusto?

    Invece, cosa accade con queste linee di codice: q=&SL e q->p=1000? Come avviene l'accesso alla variabile p? Semplicemente così:"q->p" senza passare per nessuna vtable?

    Grazie.

Permessi di invio

  • Non puoi inserire discussioni
  • Non puoi inserire repliche
  • Non puoi inserire allegati
  • Non puoi modificare i tuoi messaggi
  •  
Powered by vBulletin® Version 4.2.1
Copyright © 2024 vBulletin Solutions, Inc. All rights reserved.