2010-07-04 8 views
5

Questo è stato chiesto nell'intervista.Come scrivere own dynamic_cast

Come scrivere il proprio dynamic_cast. Penso, sulla base della funzione del nome di typeid.

Ora come implementare il proprio typid? Non ne ho idea.

+0

@ saurabh01 Torna attraverso le domande che hai chiesto e scegliere la risposta più utile (se presente). Fai clic sulla casella di controllo a sinistra della risposta per accettarla. Grazie! –

+13

Sicuramente un contendente per la domanda di intervista più esigente del mese. –

+2

@Neil Butterworth: sono d'accordo. C'è un motivo per cui DC è implementato dal compilatore. – Puppy

risposta

18

C'è un motivo per cui non si dispone di alcun indizio, dynamic_cast e static_cast non sono come const_cast o reinterpret_cast, in realtà eseguire l'aritmetica dei puntatori e sono un po 'typesafe.

L'aritmetica dei puntatori

Per illustrare questo, pensare al seguente disegno:

struct Base1 { virtual ~Base1(); char a; }; 
struct Base2 { virtual ~Base2(); char b; }; 

struct Derived: Base1, Base2 {}; 

Un'istanza di Derived dovrebbe essere simile a questa (si basa su GCC dal momento che è in realtà Compilatore dipendente ...):

|  Cell 1  | Cell 2 |  Cell 3  | Cell 4 | 
| vtable pointer | a | vtable pointer  | b | 
|   Base 1    |  Base 2    | 
|      Derived        | 

Ora pensa al lavoro necessario per c asting:

  • trasmissione da Derived a Base1 non richiede alcun lavoro supplementare, sono allo stesso indirizzo fisico
  • trasmissione da Derived a Base2 rende necessario spostare il puntatore 2 byte

Pertanto , è necessario conoscere il layout di memoria degli oggetti per poter eseguire il cast tra un oggetto derivato e uno della sua base. E questo è noto solo al compilatore, l'informazione non è accessibile attraverso alcuna API, non è standardizzata o altro.

In codice, questo si tradurrebbe come:

Derived derived; 
Base2* b2 = reinterpret_cast<Base2>(((char*)&derived) + 2); 

E questo è, ovviamente, per un static_cast.

Ora, se tu fossi in grado di utilizzare static_cast nella realizzazione di dynamic_cast, allora si potrebbe sfruttare il compilatore e lasciare gestire il puntatore aritmetico per voi ... ma non sei ancora fuori dal bosco.

Scrivere dynamic_cast?

Per prima cosa, abbiamo bisogno di chiarire le specifiche dei dynamic_cast:

  • dynamic_cast<Derived*>(&base); restituisce null se base non è un'istanza di Derived.
  • dynamic_cast<Derived&>(base); getta std::bad_cast in questo caso.
  • dynamic_cast<void*>(base); restituisce l'indirizzo della classe più derivata
  • dynamic_cast rispettare le specifiche di accesso (public, protected e private eredità)

Io non so voi, ma penso che sarà brutto .Utilizzando typeid non è sufficiente qui:

struct Base { virtual ~Base(); }; 
struct Intermediate: Base {}; 
struct Derived: Base {}; 

void func() 
{ 
    Derived derived; 
    Base& base = derived; 
    Intermediate& inter = dynamic_cast<Intermediate&>(base); // arg 
} 

Il problema qui è che typeid(base) == typeid(Derived) != typeid(Intermediate), quindi non si può fare affidamento su che o.

Un'altra cosa divertente:

struct Base { virtual ~Base(); }; 
struct Derived: virtual Base {}; 

void func(Base& base) 
{ 
    Derived& derived = static_cast<Derived&>(base); // Fails 
} 

static_cast non funziona quando è coinvolto l'ereditarietà virtuale ... così abbiamo andare un problema di aritmetica dei puntatori calcolo strisciante

quasi soluzione.

class Object 
{ 
public: 
    Object(): mMostDerived(0) {} 
    virtual ~Object() {} 

    void* GetMostDerived() const { return mMostDerived; } 

    template <class T> 
    T* dynamiccast() 
    { 
    Object const& me = *this; 
    return const_cast<T*>(me.dynamiccast()); 
    } 

    template <class T> 
    T const* dynamiccast() const 
    { 
    char const* name = typeid(T).name(); 
    derived_t::const_iterator it = mDeriveds.find(name); 
    if (it == mDeriveds.end()) { return 0; } 
    else { return reinterpret_cast<T const*>(it->second); } 
    } 

protected: 
    template <class T> 
    void add(T* t) 
    { 
    void* address = t; 
    mDerived[typeid(t).name()] = address; 
    if (mMostDerived == 0 || mMostDerived > address) { mMostDerived= address; } 
    } 

private: 
    typedef std::map < char const*, void* > derived_t; 
    void* mMostDerived; 
    derived_t mDeriveds; 
}; 

// Purposely no doing anything to help swapping... 

template <class T> 
T* dynamiccast(Object* o) { return o ? o->dynamiccast<T>() : 0; } 

template <class T> 
T const* dynamiccast(Object const* o) { return o ? o->dynamiccast<T>() : 0; } 

template <class T> 
T& dynamiccast(Object& o) 
{ 
    if (T* t = o.dynamiccast<T>()) { return t; } 
    else { throw std::bad_cast(); } 
} 

template <class T> 
T const& dynamiccast(Object const& o) 
{ 
    if (T const* t = o.dynamiccast<T>()) { return t; } 
    else { throw std::bad_cast(); } 
} 

avete bisogno di qualche piccole cose nel costruttore:

class Base: public Object 
{ 
public: 
    Base() { this->add(this); } 
}; 

Quindi, cerchiamo di controllare:

  • classic usa: va bene
  • virtual eredità? dovrebbe funzionare ... ma non testato
  • rispetto specificatori di accesso ... ARG:/

Buona fortuna a chiunque cerchi di implementare questo al di fuori del compilatore, in realtà: x

+0

Nella rappresentazione grafica del layout della classe in alto, si prega di cambiare 'Byte' in qualcos'altro perché il puntatore vtable è [quasi sicuramente] non un Byte. – iAdjunct

+1

@iAdjunct: In effetti, ho usato 'Cell' invece. –

-1

Facile. Ricava tutti gli oggetti da qualche interfaccia di tipo con una funzione virtuale WhoAmI(). Sovrascrivi in ​​tutte le classi derivate.

+0

E riguardo l'upcasting? –

1

Un modo è quello di dichiarare un identificatore statico (un numero intero ad esempio) che definisce l'ID di classe. Nella classe è possibile implementare sia le routine statiche che quelle con ambito che restituisce l'identificatore di classe (Remeber per contrassegnare le routine virtuali).

L'identificatore statico deve essere inizializzato durante l'inizializzazione dell'applicazione. Un modo è di chiamare una routine InitializeId per ogni classe, ma ciò significa che i nomi delle classi devono essere noti e il codice di inizializzazione deve essere modificato ogni volta che la gerarchia delle classi viene modificata. Un altro modo è controllare l'identificatore valido in fase di costruzione, ma questo introduce un sovraccarico poiché ogni volta che viene costruita una classe il controllo viene eseguito, ma solo la prima volta è utile (in aggiunta, se non viene costruita una classe, la routine statica non può essere utile poiché l'identificatore non è mai inizializzato).

Una fiera attuazione potrebbe essere una classe template:

template <typename T> 
class ClassId<T> 
{ 
    public: 

    static int GetClassId() { return (sClassId); } 

    virtual int GetClassId() const { return (sClassId); } 

    template<typename U> static void StateDerivation() { 
     gClassMap[ClassId<T>::GetClassId()].push_back(ClassId<U>::GetClassId()); 
    } 

    template<typename U> const U DynamicCast() const { 
     std::map<int, std::list<int>>::const_iterator it = gClassMap.find(ClassId<T>); // Base class type, with relative derivations declared with StateDerivation() 
     int id = ClassId<U>::GetClassId(); 

     if (id == ClassId<T>::GetClassId()) return (static_cast<U>(this)); 

     while (it != gClassMap.end()) { 
      for (std::list<int>::const_iterator = pit->second.begin(), pite = it->second->end(); pit != pite; pit++) { 
       if ((*pit) == id) return (static_cast<U>(this)); 
       // ... For each derived element, iterate over the stated derivations. 
       // Easy to implement with a recursive function, better if using a std::stack to avoid recursion. 
      } 
     } 

     return (null); 
    } 

    private: 

    static int sClassId; 
} 

#define CLASS_IMP(klass) static int ClassId<klass>::sClassId = gClassId++; 

// Global scope variables  

static int gClassId = 0; 
static std::map<int, std::list<int>> gClassMap; 

CLASS_IMP è definita in ogni classe per derivanti da ClassId, e gClassId e gClassMap deve essere visibile in ambito globale.

Gli identificatori di classe disponibili sono mantenuti da una singola variabile statica intera accessibile da tutte le classi (una classe base ClassID o una variabile globale), che viene incrementata ogni volta che viene assegnato un nuovo identificatore di classe.

Per rappresentare la gerarchia di classi è sufficiente una mappa tra l'identificatore di classe e le sue classi derivate. Per sapere se una classe può essere convertita in una classe specifica, scorrere sulla mappa e verificare la derivazione delle dichiarazioni.

Ci sono molte difficoltà per affrontare ... l'uso di riferimenti! derivazioni virtuali! cattivo casting! Bad tipo di classe di inizializzazione mappatura porterà ad errori di fusione ...


I rapporti tra le classi sono definite manualmente, hard-coded con la routine di inizializzazione. Ciò consente di determinare se una classe derivata da, o se due classi come una derivazione comune.

class Base : ClassId<Base> { } 
#define CLASS_IMP(Base); 
class Derived : public Base, public ClassId<Derived> { } 
#define CLASS_IMP(Derived); 
class DerivedDerived : public Derived, public ClassId<DerivedDerived> { } 
#define CLASS_IMP(DerivedDerived); 

static void DeclareDerivations() 
{ 
    ClassId<Base>::StateDerivation<Derived>(); 
    ClassId<Derived>::StateDerivation<DerivedDerived>(); 
} 

Personalmente sono d'accordo con "c'è un motivo se i compilatori implementa dynamic_cast"; probabilmente il compilatore fa le cose meglio (specialmente rispetto al codice di esempio!).

+0

Supponiamo che tu abbia scoperto di avere riferimenti a classi che appaiono nella stessa gerarchia, come stai proponendo di fare effettivamente il 'dynamic_cast'? –

+0

Aggiornato. Penso che fosse ovvio il percorso della soluzione. – Luca

+0

Sembra che tu stia usando un 'reinterpret_cast' di una specializzazione' ClassId' come risultato di 'DynamicCast'.Sicuramente questa è la lezione sbagliata da lanciare; anche se fosse la classe giusta reinterpret_cast non ha intenzione di fare gli aggiustamenti appropriati per le gerarchie MI. Ho perso qualcosa di ovvio nella tua soluzione? –

0

Per tentare una risposta un po 'meno di routine, se un po' meno definito:

Quello che dovete fare è lanciare il puntatore a un int *, creare un nuovo tipo T sullo stack, cast di un puntatore ad esso per int * e confronta il primo int in entrambi i tipi. Questo farà un confronto indirizzo vtable. Se sono dello stesso tipo, avranno lo stesso vtable. Altrimenti, non lo fanno.

Il più ragionevole di noi è solo una costante integrale nelle nostre classi.

+0

Ma questo non ti dice se è possibile 'dynamic_cast' tra due tipi o come eseguire il cast. –