Evitando puntatori vtable negli oggetti in C ++

0

Nella mia precedente domanda , è stato evidenziato che le implementazioni di C ++ come GCC devono memorizzare un puntatore vtable in ogni copia di una classe per ogni classe genitore che ha una funzione virtuale.

Quindi una classe che implementa dire 5 "interfacce" (vale a dire pure classi astratte) deve avere almeno 5 punti di misura prima che anche i suoi membri vengano aggiunti.

A prima vista, questo sembra necessario. Quando abbiamo upcast, tutto ciò che possiamo fare è cambiare il puntatore. Ma abbiamo bisogno di un nuovo vtable per quel puntatore upcasted. Quindi non c'è molta altra opzione oltre a metterlo nell'oggetto stesso.

Lo svantaggio di questo è che paghiamo un costo per oggetto per le classi genitore virtuali, anche quelle senza membri.

Che dire invece, facendo un puntatore "due puntatori". Il primo puntatore è un puntatore all'oggetto. Il secondo è un puntatore ad esso è vtable. Questo potrebbe forse essere fatto solo per gli oggetti virtuali.

L'oggetto stesso potrebbe ora essere solo dati puri, senza bisogno di puntatori vtable.

Nota quindi abbiamo bisogno solo di un puntatore vtable, anche se ci sono più genitori. Il vtable stesso potrebbe avere voci che descrivono come upcast, o forse il compilatore sa come farlo.

C'è qualche ragione per cui questo approccio non è fattibile > Esistono implementazioni C ++ che lo fanno?

(Nota: Capisco che si possano evitare basi pure astratte come interfacce e "duck typing" e nei concetti futuri, ma lasciamo ignorare per il momento)

    
posta Clinton 06.08.2018 - 06:08
fonte

4 risposte

9

How about instead, making a pointer "two pointers".

Ciò farebbe gonfiare ogni puntatore, anche quelli che non usano il polimorfismo dinamico. Ricorda: devi essere in grado di lanciare qualsiasi puntatore oggetto su void* e ritornare a quel tipo con un comportamento valido. Ciò non sarebbe possibile a meno che ogni puntatore non utilizzi il tuo approccio "a due puntatori". Inoltre, lo standard C ++ non consente di determinare la dimensione di un puntatore a oggetti in base al tipo a cui punta.

Ciò interromperà anche ABI con C e ogni funzione API C mai scritta. Le implementazioni in C ++ amano essere in grado di chiamare le API C.

C ++ è una lingua che preferisce seguire la regola "non pagare per ciò che non si usa". Gli utenti che utilizzano molte eredità multiple di interfacce virtuali ne pagano; gli utenti che non usano queste cose non devono pagare nulla. È come dovrebbe essere.

    
risposta data 06.08.2018 - 06:30
fonte
4

How about instead, making a pointer "two pointers".

Sì, penso che potrebbe essere fatto, in teoria - ci sono molti modi in cui il C ++ potrebbe essere implementato. Queste scelte sono un compromesso tra una cosa e l'altra; tuttavia, ci sono molte considerazioni da fare in tale implementazione.

C ++ evita la ricerca su più classi padre di ereditarietà capitalizzando il punto di conversione (dalla classe concreta alla base (astratta)). Altri linguaggi scelgono invece di cercare un'interfaccia nel vtable della classe concreta (e invece non fanno nulla al punto di conversione), ma sono sicuri che C ++ potrebbe fare anche quello, se lo volessero, non pensare che ci sia una regola rigida su come deve essere implementato. (FYI, Non fare nulla al punto di conversione tende ad essere un po 'più amichevole per la garbage collection, un'altra ragione per cui tende a essere favorita da C # e Java.)

Tuttavia, C ++ consente campi di istanza in classi base (astratte), mentre C # e Java non supportano i campi di istanza per le interfacce. Ciò rende le cose un po 'più complicate per C ++, dato che deve conoscere gli slot di dati nell'oggetto così come gli slot di funzione nel vtable. Forse a causa di ciò, penso, di solito i C ++ si adattano ad aggiustare il puntatore dell'oggetto al punto di conversione, piuttosto che cercare le fessure dei dati e cercare gli slot per le funzioni virtuali.

Si noti che un singolo oggetto che implementa 5 interfacce diverse è, IMHO, sopravvalutato. Potresti trovare la composizione migliore per molti scenari, nel senso che crei diverse classi che collaborano, ogni classe implementa solo un'interfaccia.

(Si noti inoltre che l'overhead che si sta descrivendo si verifica solo nella seconda classe genitore, non nella prima, poiché la sottoclasse e la sua prima base possono condividere un puntatore vtable.)

In ogni caso, il sovraccarico è minimo a meno che non si abbia intenzione di creare una grande quantità di questi oggetti. Di solito, gli oggetti creati in grandi quantità possono essere ottimizzati in vari modi, come ad esempio il modello (anche se complesso) del peso mosca.

    
risposta data 06.08.2018 - 06:53
fonte
2

How about instead, making a pointer "two pointers". The first pointer is a pointer to the object. The second is a pointer to it's vtable.

Questo è quasi esattamente il modo in cui Go implementa il polimorfismo tramite la sua meccanica interface{} .

Is there any reason this approach is not viable> Are there any C++ implementations that do this?

Esiste un approccio giocattolo definito qui che implementa un'interfaccia simile (scusa il gioco di parole), tuttavia funziona solo sui puntatori. Avrebbe bisogno di modifiche minori per supportare anche i riferimenti.

A causa della mancanza di supporto per le lingue, mentre l'utilizzo delle interfacce può essere semplificato, la definizione delle interfacce diventa molto più prolissa e ripetitiva.

Per riepilogare la definizione dell'interfaccia dal link:

struct Unknown;

class Person {
    template <class T>
    struct implemented_by {
        const char* (T::*name) ();
        void (T::*talk) ();
        static const implemented_by vtable;
    };

    const implemented_by<Unknown>* const vt;
    Unknown* const p;

  public:
    inline const char* name () { return (p->*(vt->name))(); }
    inline void talk () { return (p->*(vt->talk))(); }

    <class T>
    Person (T* x) :
      vt(reinterpret_cast<const implemented_by*>(&implemented_by<T>::vtable)),
      p(reinterpret_cast<Unknown*>(x)) {}
};

template <class T>
const Person::implemented_by<T> Person::implemented_by<T>::vtable = {
    &(T::name),
    &(T::talk)
};

Alcuni di questi potrebbero essere eliminati estraendo il vtable statico nel modello di costruzione, ma nel complesso probabilmente non è l'approccio migliore per il codice di produzione.

Mi piacerebbe vederlo pulito. Ho fatto un crack io stesso, senza successo, e non ho avuto il tempo di rivisitare il concetto. Speravo di automatizzare i membri vtable e dati tramite una combinazione di CRTP e caratteri tipografici.

    
risposta data 20.11.2018 - 15:25
fonte
0

Hai frainteso la risposta alla tua altra domanda o non hai letto la risposta di Richard Hodges.

Ogni istanza di classe C ++ possiede uno e solo un puntatore vtable.

Esiste un vtable per ogni tipo di classe, ad es. se C eredita da A e B, esiste una specifica vtable per quella combinazione.

Google "Itanium C ++ ABI" per ulteriori dettagli.

    
risposta data 11.10.2018 - 11:20
fonte

Leggi altre domande sui tag