2010-08-24 27 views
19

Necesito almacenar una lista de varias propiedades de un objeto. La propiedad consiste en un nombre y datos, que pueden ser de cualquier tipo de datos.¿Cómo almacenar diferentes tipos de datos en una lista? (C++)

Sé que puedo hacer una clase "Propiedad" y ampliarla con diferentes PropertySubClasses que solo difieren con el tipo de datos que están almacenando, pero no se siente bien.

class Property 
{ 
    Property(std::string name); 
    virtual ~Property(); 

    std::string m_name; 
}; 

class PropertyBoolean : Property 
{ 
    PropertyBoolean(std::string name, bool data); 

    bool m_data; 
}; 

class PropertyFloat : Property 
{ 
    PropertyFloat(std::string name, float data); 

    float m_data; 
}; 

class PropertyVector : Property 
{ 
    PropertyVector(std::string name, std::vector<float> data); 

    std::vector<float> m_data; 
}; 

Ahora puedo almacenar todo tipo de propiedades en un

std::vector<Property*> 

y para obtener los datos, puedo enviar el objeto de la subclase. O puedo hacer una función virtual pura para hacer algo con los datos dentro de la función sin la necesidad de lanzar.

De todos modos, no se siente bien crear este tipo diferente de subclases que solo difieren según el tipo de datos que están almacenando. ¿Hay alguna otra forma conveniente de lograr un comportamiento similar?

No tengo acceso a Boost.

+0

Similar a lo que quiere usando Boost.Variant: http://stackoverflow.com/questions/1358427/function-which-returns-an-unknown-type – AraK

+0

Boost no es una opción. – hasdf

+0

Tus clases 'PropertyXxxx' necesitan heredar de' clase Property'. Es posible que desee editar su publicación. –

Respuesta

23

C++ es un lenguaje multi-paradigma. Brilla más brillante y es más poderosa donde los paradigmas se mezclan.

class Property 
{ 
public: 
    Property(const std::string& name) //note: we don't lightly copy strings in C++ 
     : m_name(name) {} 
    virtual ~Property() {} 
private: 
    std::string m_name; 
}; 

template< typename T > 
class TypedProperty : public Property 
{ 
public: 
    TypedProperty (const std::string& name, const T& data) 
     : Property(name), m_data(data); 
private: 
    T m_data; 
}; 

typedef std::vector< std::shared_ptr<Property> > property_list_type; 

Editar: ¿Por qué utilizar std::shared_ptr<Property> en lugar de Property*?
consideran este código:

void f() 
{ 
    std::vector<Property*> my_property_list; 
    for(unsigned int u=0; u<10; ++u) 
    my_property_list.push_back(new Property(u)); 

    use_property_list(my_property_list); 

    for(std::vector<Property*>::iterator it=my_property_list.begin(); 
             it!=my_property_list.end(); ++it) 
    delete *it; 
} 

Eso for lazo allí los intentos de limpieza, eliminación de todas las propiedades en el vector, justo antes de que se sale del ámbito y toma todos los punteros con él.
Ahora, si bien esto podría parecerle excelente a un novato, si usted es un desarrollador de C++ con poca experiencia, ese código debería hacer sonar las alarmas tan pronto como lo vea.

El problema es que la llamada al use_property_list() podría arrojar una excepción. Si es así, la función f() se dejará inmediatamente. Para una limpieza adecuada, se invocarán los destructores de todos los objetos automáticos creados en f(). Es decir, my_property_list se destruirá adecuadamente. El destructor std::vector limpiará muy bien los datos que contiene.Sin embargo, contiene punteros, y ¿cómo debería saber std::vector si estos punteros son los últimos que hacen referencia a sus objetos?
Como no lo sabe, no eliminará los objetos, solo destruirá los punteros cuando destruya su contenido, dejándote con objetos en el montón a los que ya no tienes punteros. Esto es lo que se llama una "fuga".

Para evitar eso, debe detectar todas las excepciones, limpiar las propiedades y volver a lanzar la excepción. Pero luego, en diez años a partir de ahora, alguien tiene que agregar una nueva función a la aplicación 10MLoC a la que ha crecido, y, como tiene prisa, agrega código que deja esa función prematuramente cuando se cumple alguna condición. El código se prueba y funciona y no se cuelga; solo el servidor del que forma parte ahora filtra unos pocos bytes por hora, lo que hace que se bloquee debido a que se ha quedado sin memoria una vez a la semana. Encontrar eso hace que muchas horas de multa depuración.

En pocas palabras: nunca administre los recursos manualmente, siempre envuélvalos en objetos de una clase diseñada para manejar exactamente una instancia de dicho recurso. Para objetos dinámicamente asignados, esos identificadores se llaman "puntero inteligente", y el más utilizado es shared_ptr.

+0

Pero 'std :: shared_ptr' aún no forma parte de C++. Y si no se le permite usar Boost, lo más probable es que no se le permita usar las extensiones TR1 ni C++ 0x tampoco. – phlipsy

+0

@phlipsy: Ah, simplemente pasé por alto la afirmación sobre el impulso. Eso es tonto. De todos modos, si una tienda no permite impulsar, tendrá que tener su propio puntero inteligente de todos modos - [tan malo como esto suele ser] (http://stackoverflow.com/questions/1437053/boost-advocacy-help- needed/1437748 # 1437748), es mejor que no tener un puntero inteligente. Además, algunos compiladores ya son compatibles con 'std :: shared_ptr'. – sbi

+0

¿Cuál es el punto de shared_ptr aquí en comparación con el puntero normal? – hasdf

9

Una manera de nivel inferior es el uso de una unión

class Property 
    union { 
    int int_data; 
    bool bool_data; 
    std::cstring* string_data; 
    }; 
    enum { INT_PROP, BOOL_PROP, STRING_PROP } data_type; 
    // ... more smarts ... 
}; 

No sé por qué su otra solución no se siente bien, así que no sé si de esta manera se sentiría mejor para usted.

EDITAR: Un poco más de código para dar un ejemplo de uso.

Property car = collection_of_properties.head(); 
if (car.data_type == Property::INT_PROP) { 
    printf("The integer property is %d\n", car.int_data); 
} // etc. 

Probablemente pondría ese tipo de lógica en un método de la clase cuando sea posible. También tendría miembros como este constructor para mantener los datos y campo de tipo de sincronización:

Property::Property(bool value) { 
    bool_data = value; 
    data_type = BOOL_PROP; 
} 
+0

¿Podría dar más detalles? No entiendo esto – hasdf

+2

@hasdf: la unión le permite almacenar cualquiera de los tipos de componentes, pero solo uno a la vez (básicamente todos los miembros comparten la misma región de memoria). Y depende de usted utilizar el campo 'data_type' para hacer un seguimiento de qué tipo se almacena realmente. – casablanca

2

escribir una clase de plantilla Property<T> que se deriva de Property con un miembro de datos de tipo T

+0

Aún no podrá ponerlos en una matriz o en un contenedor stl. La propiedad y la propiedad son tipos completamente diferentes. – Dima

+1

@Dima: Sí, pero en el ejemplo, se almacenan por puntero de todos modos. –

+2

@Dima: No si tienen una superclase común, que creo que es a lo que jdv significa "deriva de' Propiedad' ". –

0

es probable que pueda Haga esto con la biblioteca de Boost, o podría crear una clase con un código de tipo y un puntero void a los datos, pero eso significaría renunciar a algo del tipo de seguridad de C++. En otras palabras, si tiene una propiedad "foo", cuyo valor es un número entero, y le da un valor de cadena en su lugar, el compilador no encontrará el error por usted.

Recomendaría volver a visitar su diseño y volver a evaluar si realmente necesita tanta flexibilidad. ¿Realmente necesita poder manejar propiedades de cualquier tipo? Si puede limitarlo a unos pocos tipos, puede encontrar una solución usando herencia o plantillas, sin tener que "luchar contra el idioma".

1

Otra posible solución es escribir una clase intermedia gestión de los punteros a Property clases:

class Bla { 
private: 
    Property* mp 
public: 
    explicit Bla(Property* p) : mp(p) { } 

    ~Bla() { delete p; } 

    // The standard copy constructor 
    // and assignment operator 
    // aren't sufficient in this case: 
    // They would only copy the 
    // pointer mp (shallow copy) 
    Bla(const Bla* b) : mp(b.mp->clone()) { } 

    Bla& operator = (Bla b) { // copy'n'swap trick 
    swap(b); 
    return *this; 
    } 

    void swap(Bla& b) { 
    using std::swap; // #include <algorithm> 
    swap(mp, b.mp); 
    } 

    Property* operator ->() const { 
    return mp; 
    } 

    Property& operator *() const { 
    return *mp; 
    } 
}; 

Hay que añadir un método virtual clone a sus clases devolviendo un puntero a una copia recién creada de la misma:

class StringProperty : public Property { 
// ... 
public: 
    // ... 
    virtual Property* clone() { return new StringProperty(*this); } 
    // ... 
}; 

Entonces serás capaz de hacer esto:

std::vector<Bla> v; 
v.push_back(Bla(new StringProperty("Name", "Jon Doe"))); 
// ... 
std::vector<Bla>::const_iterator i = v.begin(); 
(*i)->some_virtual_method(); 

Al salir del alcance de v, se destruirán todos los Bla s liberando automáticamente los punteros que están sosteniendo. Debido a su operador de desreferenciación e indirección sobrecargado, la clase Bla se comporta como un puntero ordinario. En la última línea *i devuelve una referencia a un objeto Bla y usar -> significa lo mismo que si fuera un puntero a un objeto Property.

Un posible inconveniente de este enfoque es que siempre obtiene una operación de almacenamiento dinámico (un new y un delete) si los objetos intermedios se deben copiar. Esto sucede, por ejemplo, si excedes la capacidad del vector y todos los objetos intermedios deben copiarse en una nueva pieza de memoria.

En el nuevo estándar (es decir, C++ 0x) podrás usar la plantilla unique_ptr: Se

  • se puede utilizar dentro de los contenedores estándar (en contraste con la auto_ptr que no deben utilizado en los contenedores estándar),
  • ofrece la semántica de movimiento generalmente más rápida (se puede pasar fácilmente) y
  • se ocupa de los punteros retenidos (los libera automáticamente).
1

veo que hay un montón de disparos a tratar de resolver su problema por ahora, pero tengo la sensación de que usted está buscando en el lado equivocado - ¿por qué realmente quiere hacer esto en el primer lugar ? ¿Hay alguna funcionalidad interesante en la clase base que haya omitido especificar?

El hecho de que se vería obligado a cambiar en un identificador de tipo de propiedad para hacer lo que quiera con una instancia específica es un olor código, especialmente cuando las subclases tienen absolutamente nada en común a través de la clase base que no sea un nombre (que es el id de tipo en este caso).

Cuestiones relacionadas