2010-03-04 12 views
7

Mi programa necesita hacer uso de void * para transportar datos u objetos en situación de invocación dinámica, para que pueda referenciar datos de tipos arbitrarios, incluso primitivos tipos. Sin embargo, recientemente descubrí que el proceso de anulación de estos espacios vacíos * en el caso de clases con múltiples clases base falla e incluso bloquea mi programa después de invocar métodos en estos punteros, incluso si las direcciones de memoria parecen ser correctas. El bloqueo ocurre durante el acceso a "vtable".herencia múltiple: resultado inesperado después del lanzamiento de void * a la segunda clase base

Así que he creado un pequeño caso de prueba, el medio ambiente es gcc 4.2 en Mac OS X:

class Shape { 
public: 
    virtual int w() = 0; 
    virtual int h() = 0; 
}; 

class Square : public Shape { 
public: 
    int l; 
    int w() {return l;} 
    int h() {return l;} 
}; 

class Decorated { 
public: 
    int padding; 
    int w() {return 2*padding;} 
    int h() {return 2*padding;} 
}; 

class DecoratedSquare : public Square, public Decorated { 
public: 
    int w() {return Square::w() + Decorated::w();} 
    int h() {return Square::h() + Decorated::h();} 
}; 


#include <iostream> 

template <class T> T shape_cast(void *vp) { 
// return dynamic_cast<T>(vp); // not possible, no pointer to class type 
// return static_cast<T>(vp); 
// return T(vp); 
// return (T)vp; 
    return reinterpret_cast<T>(vp); 
} 

int main(int argc, char *argv[]) { 
    DecoratedSquare *ds = new DecoratedSquare; 
    ds->l = 20; 
    ds->padding = 5; 
    void *dsvp = ds; 

    std::cout << "Decorated (direct)" << ds->w() << "," << ds->h() << std::endl; 

    std::cout << "Shape " << shape_cast<Shape*>(dsvp)->w() << "," << shape_cast<Shape*>(dsvp)->h() << std::endl; 
    std::cout << "Square " << shape_cast<Square*>(dsvp)->w() << "," << shape_cast<Square*>(dsvp)->h() << std::endl; 
    std::cout << "Decorated (per void*) " << shape_cast<Decorated*>(dsvp)->w() << "," << shape_cast<Decorated*>(dsvp)->h() << std::endl; 
    std::cout << "DecoratedSquare " << shape_cast<DecoratedSquare*>(dsvp)->w() << "," << shape_cast<DecoratedSquare*>(dsvp)->h() << std::endl; 
} 

produce el siguiente resultado:

Decorated (direct)30,30 
Shape 30,30 
Square 30,30 
Decorated (per void*) 73952,73952 
DecoratedSquare 30,30 

Como se puede ver, el "decorado (por nulo *) "el resultado es completamente incorrecto. También debería ser 30,30 como en la primera línea.

Cualquiera que sea el método de conversión que uso en shape_cast() siempre obtendré los mismos resultados inesperados para la parte decorada. Algo está completamente mal con este vacío *.

Desde mi comprensión de C++ esto debería estar realmente funcionando. ¿Hay alguna posibilidad de que esto funcione con el vacío *? ¿Puede ser esto un error en gcc?

Gracias

+1

Solo puede usar reinterpret_cast para convertir a void * y luego volver al tipo original. Puedes __NOT__ lanzar al vacío * y luego a cualquier otra cosa. –

+0

Sería mucho mejor utilizar el patrón de decorador que MI, pero eso no resolvería el problema de conversión de void *. – quamrana

Respuesta

7

No es un error del compilador, es lo que hace reinterpret_cast. El objeto DecoratedSquare será expuesto en la memoria algo como esto:

Square 
Decorated 
DecoratedSquare specific stuff 

Conversión de un puntero a este a void* dará la dirección del inicio de estos datos, sin conocimiento de qué tipo es allí. reinterpret_cast<Decorated*> tomará esa dirección e interpretará lo que haya allí como Decorated - pero el contenido de la memoria real es Square. Esto está mal, entonces obtienes un comportamiento indefinido.

Debe obtener los resultados correctos si reinterpret_cast es del tipo dinámico correcto (es decir, DecoratedSquare), luego conviértalo en la clase base.

+0

Eso lo explica pero significaría que sin conocimiento de la subclase concreta no puedo producir código que funcione en punteros a una de las clases base de una forma polimórfica como sería posible en situaciones de herencia única, donde la persona que llama no lo hace necesito saber la subclase concreta. En otras palabras, no sería posible compilar un marco y usarlo más tarde cuando se conozcan las subclases concretas. Eso significa que la herencia múltiple en C++ no está completa. –

+2

MI en C++ está completo, a menos que deseche la información del tipo. Puedes usar funciones 'virtuales' para obtener un comportamiento polimórfico sin conocer subclases concretas si no lanzas salvajemente a tipos incorrectos. –

+0

Para mí, como simplemente no sé el subtipo concreto en esa parte del marco, simplemente significa que no puedo usar el vacío * allí. Tengo que llevar información de tipo de alguna manera con un "cualquiera" o similar. No es agradable y no es lo que esperaba :(pero al menos me diste una buena explicación. Gracias a todos –

2

A static_cast o una dynamic_cast en presencia de herencia múltiple puede cambiar la representación del puntero por compensación de modo que designar la dirección correcta. static_cast determina el desplazamiento correcto considerando la información de tipeo estático. dynamic_cast lo hace al verificar el tipo dinámico. Si elige throw void *, está perdiendo toda la información de tipeo estático y la posibilidad de obtener información de tipeo dinámico, por lo que reinterpret_cast está utilizando asumiendo que el desplazamiento es nulo, fallando algunas veces.

+0

¿Esto significa efectivamente que void * no es factible en situaciones de herencia múltiple? –

+1

Es factible, siempre y cuando se proyecte siempre al tipo real del objeto, no a una clase base. –

+0

ver mi comentario en la respuesta de Mike –

9

Repita diez veces: lo único que puede hacer con seguridad con un puntero reinterpret_cast es reinterpret_cast de nuevo al mismo tipo de puntero de donde vino. Lo mismo se aplica a las conversiones a void*: debe volver a convertir al tipo original.

Por lo tanto, si lanza un DecoratedSquare* al void*, debe devolverlo al DecoratedSquare*. No Decorated*, no Square*, no Shape*. Algunos de ellos podrían funcionar en su máquina, pero esta es una combinación de buena suerte y comportamiento específico de implementación.Por lo general, funciona con herencia única, porque no hay una razón obvia para implementar indicadores de objeto de una manera que lo detenga, pero esto no está garantizado y no puede funcionar en general para la herencia múltiple.

Usted dice que su código accede a "tipos arbitrarios, incluidos los tipos primitivos" a través de un vacío *. No hay nada de malo en esto, presumiblemente, quien recibe los datos sabe tratarlo como DecoratedSquare* y no como, por ejemplo, int*.

Si el que recibe sólo sabe tratarla como una clase base, tal como Decorated*, entonces el que lo convierte en void* debe static_cast a la clase base primero, y luego a void*:

void *decorated_vp = static_cast<Decorated*>(ds); 

Ahora, cuando echaste decorated_vp a Decorated*, obtendrás el resultado de static_cast<Decorated*>(ds), que es lo que necesitas.

+0

Steve gracias por esto. Lo resumí todo. De hecho, siempre se garantiza que en la parte de llamada del marco hay un tipo de puntero base conocido que se convierte en void * y luego se transporta al extremo receptor, que conoce dos cosas: la clase base superior y alguna subclase donde un método se define. No sabe, sin embargo, la subclase concreta. Pero esto parece estar bien ahora, ya que el extremo receptor siempre reinterpreta_casts a la misma clase base superior que el extremo emisor y luego realiza un dynamic_cast o static_cast a la subclase intermedia donde invoca el método deseado. –

+0

+1 porque "lo único que puede hacer con seguridad con un puntero reinterpret_cast es reinterpretarlo de nuevo al mismo tipo de puntero de donde vino". Resume muy bien la situación. –

Cuestiones relacionadas