2010-07-27 14 views
14

Actualmente estoy trabajando en la integración de un paquete de terceros que usa muchas cosas de RTTI en una plataforma que no es RTTI (Android). Básicamente, hice mi propia implementación RTTI pero estoy atascado en un problema.C++: descifrar un objeto heredado en forma de diamante sin RTTI/dynamic_cast

El problema es que muchas clases tienen el problema de herencia de diamantes ya que todas las clases derivan de la misma clase base (objeto) ... y entonces, si quiero bajar de la clase base a la clase derivada, Tengo que usar un dynamic_cast - ¡pero RTTI no está disponible! ¿Cómo convierto un objeto de principal a secundario cuando hay herencia virtual sin dynamic_cast?

Parece que:

class A 
{ 
public: 
virtual char* func() { return "A"; }; 
}; 
class B : public virtual A 
{ 
public: 
//virtual char* func() { return "B"; }; 
}; 
class C : public virtual A 
{ 
public: 
//virtual char* func() { return "C"; }; 
}; 

class D : public B, public C 
{ 
public: 
//virtual char* func() { return "D"; }; 
}; 

D d; 
A* pa = static_cast<A*>(&d); 
D* pd = static_cast<D*>(pa); // can't do that! dynamic_cast does work though... 

Esos son mis errores:

error C2635: no se puede convertir un 'A *' a una 'D *'; conversión de una clase base virtual está implícito

error C2440: 'inicialización': no ​​se puede convertir de 'test_convert :: A *' a 'test_convert :: D *' moldeada desde la base hasta deriva requiere dynamic_cast o static_cast

¿Alguna idea?

+0

heh, bueno MS acaba de decir que elimine la palabra clave virtual del código y resolverá el problema. Ver su documentación para el error, no estoy bromeando. –

+1

¿Quién demonios te dijo que Android es una plataforma que no es rtti? El NDK r5 y más nuevo debería ser compatible con RTTI (creo que debes activarlo con '-frtti', pero debería funcionar entonces). Incluso para plataformas más antiguas, ya que está todo vinculado estáticamente. –

Respuesta

12

Solo puede hacer esto con dynamic_cast; ningún otro elenco hará esto.

Si no puede diseñar sus interfaces para que no necesite realizar este tipo de conversión, lo único que puede hacer es hacer que la función de conversión forme parte de su jerarquía de clases.

E.g. (Horriblemente hacky)

class D; 

class A 
{ 
public: 
    virtual D* GetDPtr() { return 0; } 
}; 

class B : public virtual A 
{ 
}; 

class C : public virtual A 
{ 
}; 

class D : public B, public C 
{ 
public: 
    virtual D* GetDPtr() { return this; } 
}; 
+3

No sé si a menos o agregar para esta respuesta. –

+2

@Noah: ¡En mi defensa, dije "horriblemente hacky"! –

+2

@Noah: la versión hacky se ve mejor que una matriz de cast que he visto en un proyecto hace mucho tiempo. En el sistema rtti-less las personas no encontraron nada mejor que dar a cada clase una identificación única y usar la identificación como un índice en una matriz de dos dimensiones. matrix [cast_what] [cast_to] era un id de la clase, el resultado de dynamic_cast. – Dummy00001

-2

Como siempre y cuando tenga otra manera de asegurarse de que lo que está haciendo es escribir seguro en tiempo de ejecución, sólo tiene que utilizar reinterpret_cast.

Básicamente es lo mismo que un elenco de estilo C, solo utilícelo si es necesario, pero permitirá que se compile el código anterior.

+1

Bueno, lo he intentado pero recibí un mensaje de advertencia (4946: reinterpret_cast usado entre clases relacionadas: 'class1' y 'class2'). Tenga en cuenta que la advertencia está deshabilitada de forma predeterminada. Creo que reinterpret_cast simplemente reinterpreta el puntero (como dice) pero no lo emite y causa problemas cuando hay herencias virtuales o múltiples padres, ya que la conversión a uno de los padres u otro puede modificar el valor del puntero. Como tal, el reinterpret_cast no funcionará en mi caso en el que el tercero que uso haga un uso extensivo de esos conceptos (padres múltiples, herencia virutal, etc.). – Adam

3

En la mayoría de los casos, el patrón visitante se puede usar para evitar downcasts. Se puede usar para evitar dynamic_cast, también.

Algunas advertencias:

1) Debe ser posible cambiar las clases ofensivas.
2) Es posible que necesite conocer CADA clase derivada.
3) Se debe saber que los objetos derivan al menos de la clase base, no se puede intentar lanzar tipos completamente no relacionados. (Esto parece cumplirse: "Quiero bajar de la clase base a la clase derivada")

En el siguiente ejemplo, utilicé plantillas. Estos pueden ser eliminados fácilmente, pero requerirían bastante esfuerzo de escritura.

class A; 
class B; 
class C; 
class D; 

// completely abstract Visitor-baseclass. 
// each visit-method must return whether it handled the object 
class Visitor 
{ 
public: 
    virtual bool visit(A&) = 0; 
    virtual bool visit(B&) = 0; 
    virtual bool visit(C&) = 0; 
    virtual bool visit(D&) = 0; 
}; 

class A 
{ 
public: 
    virtual const char* func() { return "A"; }; 
    virtual void accept(Visitor& visitor) { visitor.visit(*this); } 
}; 
class B : public virtual A 
{ 
public: 
    virtual const char* func() { return "B"; }; 
    virtual void accept(Visitor& visitor) { visitor.visit(*this); } 
}; 
class C : public virtual A 
{ 
public: 
    virtual const char* func() { return "C"; }; 
    virtual void accept(Visitor& visitor) { visitor.visit(*this); } 
}; 
class D : public B, public C 
{ 
public: 
    virtual const char* func() { return "D"; }; 
    virtual void accept(Visitor& visitor) { visitor.visit(*this); } 
}; 

// implementation-superclass for visitors: 
// each visit-method is implemented and calls the visit-method with the parent-type(s) 
class InheritanceVisitor : public Visitor 
{ 
    virtual bool visit(A& a) { return false; } 
    virtual bool visit(B& b) { return visit(static_cast<A&>(b)); } 
    virtual bool visit(C& c) { return visit(static_cast<A&>(c)); } 
    virtual bool visit(D& d) { return visit(static_cast<B&>(d)) || visit(static_cast<C&>(d)); } 
}; 

template<typename T> // T must derive from A 
class DerivedCastVisitor : public InheritanceVisitor 
{ 
public: 
    DerivedCastVisitor(T*& casted) : m_casted(casted) {} 
    virtual bool visit(T& t) 
    { m_casted = &t; return true; } 
private: 
    T*& m_casted; 
}; 

// If obj is derived from type T, then obj is casted to T* and returned. 
// Else NULL is returned. 
template<typename T> 
T* derived_cast(A* obj) 
{ 
    T* t = NULL; 
    if (obj) 
    { 
    DerivedCastVisitor<T> visitor(t); 
    obj->accept(visitor); 
    } 
    return t; 
} 

int main(int argc, char** argv) 
{ 
    std::auto_ptr<A> a(new A); 
    std::auto_ptr<A> b(new B); 
    std::auto_ptr<A> c(new C); 
    std::auto_ptr<A> d(new D); 

    assert(derived_cast<A>(a.get()) != NULL); // a has exact type A 
    assert(derived_cast<B>(b.get()) != NULL); // b has exact type B 
    assert(derived_cast<A>(b.get()) != NULL); // b is derived of A 
    assert(derived_cast<C>(b.get()) == NULL); // b is not derived of C 
    assert(derived_cast<D>(d.get()) != NULL); // d has exact type D 
    assert(derived_cast<B>(d.get()) != NULL); // d is derived of B 
    assert(derived_cast<C>(d.get()) != NULL); // d is derived of C, too 
    assert(derived_cast<D>(c.get()) == NULL); // c is not derived of D 

    return 0; 
} 
0

El problema con la herencia virtual es que la dirección de la clase base no es necesariamente la misma que la dirección derivada. Por lo tanto, incluso reinterpret_cast o void* moldes será no ayuda.

Una forma de resolver esto sin usar dynamic_cast es calcular el desplazamiento entre ambos tipos de punteros (el tipo exacto y el tipo de ref) para modificar la dirección del objeto en consecuencia durante el reparto.

template <typename E, typename T> 
E& force_exact(const T& ref) 
{ 
    static const E* exact_obj; 
    static const T& exact_obj_ref = *exact_obj; 
    static const ptrdiff_t exact_offset = 
    (const char*)(void*)(&exact_obj_ref) 
    - (const char*)(void*)(exact_obj); 
    return *(E*)((char*)(&ref) - exact_offset); 
} 
+0

Nota al margen: La declaración del desplazamiento se hace estática porque debe calcularse solo una vez para cada par T, E en teoría. Sin embargo, en la práctica, usar 'static' tiene un costo de tiempo de ejecución (reescrito como' if (! Var_initilized) var_ = ... '). Una forma de salir de esto es crear una instancia de una clase de plantilla que almacene de manera estática el valor de compensación para cada pareja T, E, pero esa es otra historia ... – log0

1

el código:

template <typename E, typename T> 
E& force_exact(const T& ref) 
{ 
    static const E* exact_obj; 
    static const T& exact_obj_ref = *exact_obj; 
    static const ptrdiff_t exact_offset = ... 

no funciona muy bien para mí como static const E* exact_obj es cero, por lo estático const T& exact_obj_ref = *exact_obj derefs cero, también, y por lo tanto se convierte en static const ptrdiff_t exact_offset también cero.

Me parece que la clase derivada necesita ser instanciada (lo que puede ser un problema para las clases abstractas ...). Entonces mi código es:

template <typename D, typename B> 
D & Cast2Derived(B & b) 
{ static D d; 
    static D * pD = & d; 
    static B * pB = pD; 
    static ptrdiff_t off = (char *) pB - (char *) pD; 

    return * (D *) ((char *) & b - off); 
} 

Probado en MSVC 2008, WinXP 32b.

Cualquier comentario/mejor solución (es) son bienvenidos.

LuP

+0

Esto es problemático si el constructor de D's incurre en efectos secundarios. Tal vez la mejor solución es reinterpretar_castrar un búfer de caracteres adecuadamente alineado y alineado al puntero D. Además, para seguir con las convocatorias de nombres de casting de C++, ¿qué le parece nombrarlo 'virtual_cast (B *)'? Todavía no lo he pensado bien, pero ¿hay algunas jerarquías de herencia por las cuales esto fracasará? Tal vez una clase que hereda de la misma clase virtual varias veces (aunque sea indirectamente) o quizás con una combinación de herencia no virtual para arrancar. De todos modos +1 –

+0

Hola Thomas, ¿podrías describir mejor tu idea sobre la reinterpretación de la conversión del búfer de char a D *? – Juster

4

Android hace apoyo RTTI. Necesita la última versión de NDK (al menos r5, la última es r6) y necesita compilar contra GNU stdlibC++ en lugar de la predeterminada.

Incluso antes, estaba la reconstrucción de CrystaX que admitía excepciones y rtti (teníamos que usar eso hasta NDK r5c oficial porque r5a y r5b tenían el soporte, pero se bloqueaban en sistemas anteriores (anteriores a 2.3)).

PD: Alguien realmente debería prohibir a los proveedores decir que admiten C++ cuando no admiten excepciones y rtti, porque la mayoría de las bibliotecas estándar, y eso es parte del estándar C++, tampoco funcionan. Además, no es tonto, especialmente para las excepciones, porque el código con excepciones es más eficiente que uno sin (siempre que se utilicen correctamente para señalar casos excepcionales).

Cuestiones relacionadas