2009-03-03 18 views
7

En Java, puede tener una Lista de objetos. Puede agregar objetos de varios tipos, luego recuperarlos, verificar su tipo y realizar la acción adecuada para ese tipo.
Por ejemplo: (disculpas si el código no es exactamente correcto, voy de la memoria)Almacenar una lista de objetos arbitrarios en C++

List<Object> list = new LinkedList<Object>(); 

list.add("Hello World!"); 
list.add(7); 
list.add(true); 

for (object o : list) 
{ 
    if (o instanceof int) 
     ; // Do stuff if it's an int 
    else if (o instanceof String) 
     ; // Do stuff if it's a string 
    else if (o instanceof boolean) 
     ; // Do stuff if it's a boolean 
} 

¿Cuál es la mejor manera de replicar este comportamiento en C++?

+0

¿Qué tipo de uso tienen los programadores Java para contenedores heterogéneos? –

+0

99% de las veces es una mala idea, pero he visto casos en los que funciona. Un ejemplo es una colección de tipo A, pero algunos objetos también pueden implementar el tipo B (es decir, Cerrar). Puede realizar 1 op en toda la colección, pero solo .close() los que se pueden cerrar. –

+0

@Outlaw: en ese caso, mantendría una colección separada con punteros de tipo B * que apuntan a objetos en la colección "principal" y perforamos .close() en la segunda colección. Más rápido y más seguro que determinar el tipo en tiempo de ejecución. –

Respuesta

14

Su ejemplo usando Boost.Variante y un visitante:

#include <string> 
#include <list> 
#include <boost/variant.hpp> 
#include <boost/foreach.hpp> 

using namespace std; 
using namespace boost; 

typedef variant<string, int, bool> object; 

struct vis : public static_visitor<> 
{ 
    void operator() (string s) const { /* do string stuff */ } 
    void operator() (int i) const { /* do int stuff */ } 
    void operator() (bool b) const { /* do bool stuff */ }  
}; 

int main() 
{ 
    list<object> List; 

    List.push_back("Hello World!"); 
    List.push_back(7); 
    List.push_back(true); 

    BOOST_FOREACH (object& o, List) { 
     apply_visitor(vis(), o); 
    } 

    return 0; 
} 

Una cosa buena acerca del uso de esta técnica es que si, más adelante, se agrega otro tipo de la variante y se olvida de modificar un visitante a incluir ese tipo, que no se compilará. Usted tiene para admitir todos los casos posibles. Mientras que, si utiliza un cambio o una cascada en las declaraciones, es fácil olvidarse de realizar el cambio en todas partes e introducir un error.

+0

+1. El recurso de visitante de la variante es * muy * bueno: incluso puede definir un único operador de plantilla() para tipos no relacionados que admiten una interfaz común. (Por ejemplo, un operador con plantilla (T t) que devuelve "t + t" funcionaría en una variante ). Los tipos no conformes se pueden manejar mediante especializaciones. –

+0

ahora que esta respuesta aparece en la parte superior, ¿puede arreglarla para que defina el vis externo? No puede definir el tipo localmente (las clases locales no se pueden usar como argumentos de plantilla, pero el suyo es (como argumento para apply_visitor). De lo contrario, creo que sí +1 –

+0

@litb - ¿De verdad? Siempre lo defino localmente para mantenerlo cerca de la llamada para apply_visitor(). – Ferruccio

13

C++ no admite contenedores heterogéneos.

Si no va a utilizar boost el truco es crear una clase ficticia y tener todas las clases diferentes derivadas de esta clase ficticia. Cree un contenedor de su elección para mantener los objetos de la clase ficticia y ya está listo para comenzar.

class Dummy { 
    virtual void whoami() = 0; 
}; 

class Lizard : public Dummy { 
    virtual void whoami() { std::cout << "I'm a lizard!\n"; } 
}; 


class Transporter : public Dummy { 
    virtual void whoami() { std::cout << "I'm Jason Statham!\n"; } 
}; 

int main() { 
    std::list<Dummy*> hateList; 
    hateList.insert(new Transporter()); 
    hateList.insert(new Lizard()); 

    std::for_each(hateList.begin(), hateList.end(), 
       std::mem_fun(&Dummy::whoami)); 
    // yes, I'm leaking memory, but that's besides the point 
} 

Si va a utilizar boost puede probar boost::any. Here es un ejemplo del uso de boost::any.

Puede encontrar este excelente article por dos destacados expertos en C++ de interés.

Ahora, boost::variant es otra cosa a tener en cuenta como j_random_hacker mencionado. Entonces, aquí hay un comparison para tener una idea clara de qué usar.

Con un boost::variant el código anterior sería algo como esto:

class Lizard { 
    void whoami() { std::cout << "I'm a lizard!\n"; } 
}; 

class Transporter { 
    void whoami() { std::cout << "I'm Jason Statham!\n"; } 
}; 

int main() { 

    std::vector< boost::variant<Lizard, Transporter> > hateList; 

    hateList.push_back(Lizard()); 
    hateList.push_back(Transporter()); 

    std::for_each(hateList.begin(), hateList.end(), std::mem_fun(&Dummy::whoami)); 
} 
+0

Sí, yo también iría con boost :: any too. es la biblioteca más apropiada para su propósito (si quiere usar * any * escriba no solo un conjunto fijo de ellos). cualquiera es como 100 líneas de código para que pueda crearlo él mismo con bastante facilidad) –

+0

este documento lo explica: http://www.two-sdg.demon.co.uk/curbralan/papers/ValuedConversions.pdf :) –

+0

Parece un fabuloso leer, ¡muchas gracias! – dirkgently

0

Bueno, se podría crear una clase base y luego crear clases que heredan de ella. Luego, guárdelos en un std :: vector.

+0

Eso tendría que ser "almacenarlos para ellos" –

+0

Buen punto Neil. –

1

estoy bastante inexperto, pero aquí es lo que me gustaría ir con-

  1. Crear una clase base para todas las clases que necesita para manipular.
  2. Escribir clase contenedor clase de contenedor de reutilización. (Revisado después de ver otras respuestas -Mi punto anterior era demasiado críptico.)
  3. Escriba un código similar.

Estoy seguro de que es posible una solución mucho mejor. También estoy seguro de que es posible una mejor explicación. Aprendí que tengo algunos malos hábitos de programación C++, así que traté de transmitir mi idea sin entrar en el código.

Espero que esto ayude.

+0

¡No seas tan duro contigo mismo! :) El enfoque de "clases base + funciones virtuales" es generalmente la mejor manera de hacer las cosas si estás diseñando tu sistema desde cero. +1. –

2

Lamentablemente, no hay una manera fácil de hacerlo en C++. Tienes que crear una clase base y derivar todas las otras clases de esta clase. Cree un vector de punteros de clase base y luego use dynamic_cast (que viene con su propia sobrecarga de tiempo de ejecución) para encontrar el tipo real.

+0

+1 porque dynamic_cast <> se aproxima mucho al enfoque de "instanciaof" del asker, pero en realidad es mejor usar una función virtual en lugar de un montón de instrucciones "if (dynamic_cast (p)) {...}". De esta forma, puede agregar todas las funciones necesarias en 1 lugar al agregar un nuevo tipo. –

+0

En el segundo pensamiento, cambie "vector de esta clase base" a "vector de punteros a clase base". El uso de un vector , como sugerirás, dará lugar al corte de objetos. (Gracias a Neil Butterworth por señalar esto en una respuesta diferente.) –

20

boost::variant es similar a la sugerencia de dirkgently de boost::any, pero admite el patrón Visitor, lo que significa que es más fácil agregar código específico del tipo más adelante. Además, asigna valores en la pila en lugar de utilizar la asignación dinámica, lo que lleva a un código ligeramente más eficiente.

EDIT: Como litb señala en los comentarios, usando variant en lugar de any significa que sólo puede contener valores de una lista pre-especificado de un de los tipos. Esto a menudo es una fortaleza, aunque podría ser una debilidad en el caso del asesino.

Aquí es un ejemplo (no usar el patrón del visitante sin embargo):

#include <vector> 
#include <string> 
#include <boost/variant.hpp> 

using namespace std; 
using namespace boost; 

... 

vector<variant<int, string, bool> > v; 

for (int i = 0; i < v.size(); ++i) { 
    if (int* pi = get<int>(v[i])) { 
     // Do stuff with *pi 
    } else if (string* si = get<string>(v[i])) { 
     // Do stuff with *si 
    } else if (bool* bi = get<bool>(v[i])) { 
     // Do stuff with *bi 
    } 
} 

(Y sí, se debe utilizar técnicamente vector<T>::size_type en lugar de int para i 's tipo, y se debe utilizar técnicamente vector<T>::iterator en lugar de todos modos , pero estoy tratando de mantenerlo simple.)

+0

+1: boost :: variant parece un ajuste perfecto para el ejemplo del OP. Para puntos extra: sería bueno mostrar cómo se escribiría este ejemplo con boost :: variant. –

+0

+1. Me ganaste :( – dirkgently

+0

Nah! Deberíamos usar 'auto's: D – dirkgently

0

La respuesta corta es ... no se puede.

La respuesta larga es ... usted debería definir su propia nueva jerarquía de objetos que todos heredan de un objeto base. En Java, todos los objetos descienden finalmente de "Objeto", que es lo que te permite hacer esto.

+0

En realidad se puede, más o menos. Por favor, mira dirkgently's y mis respuestas. –

+0

Aunque el enfoque que se discute aquí * se * usa en lugares. Ver http://root.cern.ch/ por ejemplo. – dmckee

+0

Ah sí ... Debería haber sabido que Boost haría esto. Sin embargo, me gusta más la respuesta de David Thornley. Si estás haciendo esto en código C++, básicamente significa que estás haciendo algo mal para empezar. – Bryan

12

¿Con qué frecuencia es realmente útil? He estado programando en C++ durante bastantes años, en diferentes proyectos, y en realidad nunca he querido un contenedor heterogéneo. Por alguna razón, puede ser común en Java (tengo mucha menos experiencia con Java), pero para cualquier uso dado en un proyecto Java podría haber una manera de hacer algo diferente que funcione mejor en C++.

C++ tiene un mayor énfasis en la seguridad del tipo que Java, y esto es muy inseguro.

Dicho esto, si los objetos no tienen nada en común, ¿por qué los guardan juntos?

Si tienen cosas en común, puede crear una clase para que hereden; alternativamente, use boost :: any. Si heredan, tienen funciones virtuales para llamar, o usan dynamic_cast <> si realmente tiene que hacerlo.

+0

+1 Las funciones virtuales se introdujeron exactamente para evitar este tipo de código: basta con activar un indicador de tipo y hacer algo diferente en cada rama de caso. –

+0

He trabajado en un sistema donde el arquitecto original hizo esto (clase de objeto base de la que todo hereda). No ayudó, y francamente se interpuso en el camino. Pasamos años eliminándolo. –

+0

+1 Tal vez soy un tonto, pero no puedo imaginar por qué querría almacenar tipos no relacionados en el mismo contenedor. Parece un puente que construyes cuando te das cuenta de que has construido tu camino por el lado equivocado de un abismo. –

0

RTTI (información de tipo de tiempo de ejecución) en C++ siempre ha sido difícil, especialmente el compilador cruzado.

Eres mejor opción es utilizar STL y definir una interfaz con el fin de determinar el tipo de objeto:

public class IThing 
{ 
    virtual bool isA(const char* typeName); 
} 

void myFunc() 
{ 
    std::vector<IThing> things; 

    // ... 

    things.add(new FrogThing()); 
    things.add(new LizardThing()); 

    // ... 

    for (int i = 0; i < things.length(); i++) 
    { 
     IThing* pThing = things[i]; 

     if (pThing->isA("lizard")) 
     { 
     // do this 
     } 
     // etc 
    } 
} 

Mike

+0

Hmm. Su interfaz está bien, pero básicamente es una reimplementación de RTTI y, como yo lo veo, si desea una interacción de compilación cruzada, usar este código (a diferencia de RTTI) no ayudará mucho ya que cada compilador cambia los nombres de forma diferente. ¿Puedes sugerir un caso en el que marcaría la diferencia? –

+0

No estaba sugiriendo la necesidad de llamar código a múltiples compiladores. Acabo de decir que descubrí que diferentes compiladores hacen RTTI de manera diferente y, como no estaba seguro de cuál estaba utilizando, escogí un enfoque independiente del compilador. –

+0

Cualquier compilador que reclame la conformidad estándar debe soportar los fundamentos tales como las comparaciones == y! = Entre objetos de tipo type_info (el tipo devuelto por typeid()). Solo estoy familiarizado con g ++ y MSVC++, que lo hacen bien, pero estoy interesado en conocer otros compiladores que fallan a este respecto. –

1

lado del hecho, ya que la mayoría han señalado, se puede' Hacer eso, o más importante, más que probable, realmente no quieres.

Eliminemos su ejemplo y consideremos algo más parecido a un ejemplo de la vida real. Específicamente, algún código que vi en un proyecto de código abierto real. Intentó emular una CPU en una matriz de caracteres. Por lo tanto, pondría en la matriz un "código de operación" de un byte, seguido de 0, 1 o 2 bytes que podría ser un carácter, un entero o un puntero a una cadena, en función del código de operación. Para manejar eso, implicó un montón de manipulación de bits.

Mi solución simple: 4 pilas separadas <> s: Una para la enumeración "opcode" y otra para chars, ints y string. Tome el siguiente de la pila de opcode, y le tomaría cuál de los otros tres para obtener el operando.

Hay una gran posibilidad de que su problema real se pueda manejar de manera similar.

+0

Había considerado este enfoque, pero me parece bastante intrincado y difícil de mantener sin saber de antemano el enfoque utilizado. – Whatsit

+0

Además, lo ÚNICO que decide que la próxima pila sea utilizada sería el tipo, por lo que necesitaría una enumeración adicional para representar eso, que de nuevo tendría que modificarse para los nuevos tipos. Afortunadamente, hay algunas soluciones más elegantes presentadas aquí. – Whatsit

2

Solo para completar este tema, quiero mencionar que en realidad se puede hacer esto con C pura usando nulo * y luego fundiéndolo en lo que tiene que ser (vale, mi ejemplo no es C puro ya que usa vectores, pero eso me ahorra un poco de código). Esto funcionará si sabe de qué tipo son sus objetos, o si almacena un campo en algún lugar que lo recuerda. Que sin duda NO quiere hacer esto, pero aquí es un ejemplo para mostrar que es posible:

#include <iostream> 
#include <vector> 

using namespace std; 

int main() { 

    int a = 4; 
    string str = "hello"; 

    vector<void*> list; 
    list.push_back((void*) &a); 
    list.push_back((void*) &str); 

    cout << * (int*) list[0] << "\t" << * (string*) list[1] << endl; 

    return 0; 
} 
+0

pero no puedes consultar el digite el id requerido por la pregunta. boost :: any es el camino a seguir. – obecalp

+0

+1. void * se puede usar cuando conoces el tipo, pero tienes razón al sugerirlo como último recurso porque destruyes la información del tipo, lo que dificulta que el compilador detecte errores. También hay un caso complicado con herencia múltiple: si "clase D: B1, B2 {} d;", ​​(vacío *) (B1 *) & d! = (Vacío *) (B2 *) & d. –

2

Aunque no se puede almacenar tipos primitivos en contenedores, se pueden crear clases de tipo de envoltura primitivas que será similar a Java tipos primitivos autoboxados (en su ejemplo, los literales primitivos escritos se están autocapturando); las instancias de las cuales aparecen en el código C++ (y pueden (casi) usarse) al igual que las variables primitivas/miembros de datos.

Ver Object Wrappers for the Built-In Types de Data Structures and Algorithms with Object-Oriented Design Patterns in C++.

Con el objeto envuelto puede usar el operador typeid() de C++ para comparar el tipo. Estoy bastante seguro de que la siguiente comparación funcionará: if (typeid(o) == typeid(Int)) [donde Int sería la clase envuelta para el tipo de primitiva de tipo int, etc ...] (de lo contrario, simplemente agregue una función a sus envoltorios primitivos que devuelva un typeid y así: if (o.get_typeid() == typeid(Int)) ...

dicho esto, con respecto a su ejemplo, esto tiene olor código para mí. a menos que este es el único lugar donde se está comprobando el tipo del objeto, me inclinaría a usar polimorfismo (especialmente si tiene otros métodos/funciones específicas con respecto al tipo). En este caso usaría los envoltorios primitivos agregando una clase interconectada declarando el método diferido (para hacer 'cosas') que w debería ser implementado por cada una de sus clases primitivas envueltas. Con esto, usted podría usar su iterador contenedor y eliminar su declaración if (nuevamente, si solo tiene esta comparación de tipo, configurar el método diferido usando polimorfismo solo por esto sería excesivo).

+0

En el caso real, solo hay un lugar donde se verifica el tipo, pero gracias por las sugerencias y referencias. – Whatsit

3

Me gustaría señalar que el uso de fundición de tipo dinámico para derivar en función del tipo a menudo insinúa fallas en la arquitectura. La mayoría de las veces puede lograr el mismo efecto usando funciones virtuales:

class MyData 
{ 
public: 
    // base classes of polymorphic types should have a virtual destructor 
    virtual ~MyData() {} 

    // hand off to protected implementation in derived classes 
    void DoSomething() { this->OnDoSomething(); } 

protected: 
    // abstract, force implementation in derived classes 
    virtual void OnDoSomething() = 0; 
}; 

class MyIntData : public MyData 
{ 
protected: 
    // do something to int data 
    virtual void OnDoSomething() { ... } 
private: 
    int data; 
}; 

class MyComplexData : public MyData 
{ 
protected: 
    // do something to Complex data 
    virtual void OnDoSomething() { ... } 
private: 
    Complex data; 
}; 

void main() 
{ 
    // alloc data objects 
    MyData* myData[ 2 ] = 
    { 
    new MyIntData() 
    , new MyComplexData() 
    }; 

    // process data objects 
    for (int i = 0; i < 2; ++i) // for each data object 
    { 
    myData[ i ]->DoSomething(); // no type cast needed 
    } 

    // delete data objects 
    delete myData[0]; 
    delete myData[1]; 
}; 
Cuestiones relacionadas