2009-07-08 19 views
27

imagine que tengo un montón de clases relacionadas con C++ (todas extendiendo la misma clase base y proporcionando el mismo constructor) que declare en un archivo de encabezado común (que incluyo), y sus implementaciones en algunos otros archivos (que compilo y vincular estáticamente como parte de la compilación de mi programa).¿Crear una instancia de clase desde su nombre?

Me gustaría poder crear una instancia de uno de ellos pasando el nombre, que es un parámetro que se debe pasar a mi programa (ya sea como línea de comando o como macro de compilación).

La única solución posible que veo es utilizar una macro:

#ifndef CLASS_NAME 
#define CLASS_NAME MyDefaultClassToUse 
#endif 

BaseClass* o = new CLASS_NAME(param1, param2, ..); 

Es el único enfoque valioso?

Respuesta

39

Este es un problema que comúnmente se resuelve utilizando la Registry Pattern:

Esta es la situación que el patrón Registro describe:

Los objetos deben ponerse en contacto con otro objeto , sabiendo únicamente el nombre del objeto o el nombre del servicio que proporciona, pero no cómo contactarlo. Proporcione un servicio que toma el nombre de un objeto, servicio o rol y devuelve un proxy remoto que encapsula el conocimiento de cómo contactar el objeto nombrado.

Es lo mismo básica publicar/encontrar modelo que forma la base de una arquitectura orientada a servicios (SOA) y para la capa de servicios en OSGi.

Implementa un registro normalmente utilizando un objeto singleton, el objeto singleton se informa en tiempo de compilación o en el momento de inicio los nombres de los objetos y la forma de construirlos. Entonces puede usarlo para crear el objeto a pedido.

Por ejemplo:

template<class T> 
class Registry 
{ 
    typedef boost::function0<T *> Creator; 
    typedef std::map<std::string, Creator> Creators; 
    Creators _creators; 

    public: 
    void register(const std::string &className, const Creator &creator); 
    T *create(const std::string &className); 
} 

registrar el nombre de los objetos y las funciones de creación de este modo:

Registry<I> registry; 
registry.register("MyClass", &MyClass::Creator); 

std::auto_ptr<T> myT(registry.create("MyClass")); 

entonces podríamos simplificar esto con macros inteligentes para que pueda hacerse en tiempo de compilación. ATL utiliza el patrón de registro para coclases que se pueden crear en tiempo de ejecución por su nombre - el registro es tan simple como usar algo como el siguiente código:

OBJECT_ENTRY_AUTO(someClassID, SomeClassName); 

Esta macro se coloca en el archivo de cabecera en alguna parte, la magia hace que se estar registrado con el singleton en el momento en que se inicia el servidor COM.

2

En C++, esta decisión debe tomarse en tiempo de compilación.

Durante el tiempo de compilación se puede utilizar un typedef en lugar de un Macor:

typedef DefaultClass MyDefaultClassToUse; 

esto es equivalente y evita una macro (macros mal ;-)).

Si la decisión se toma durante el tiempo de ejecución, necesita escribir su propio código para admitirlo. La solución simple es una función que prueba la cadena y crea una instancia de la clase respectiva.

Una versión extendida de eso (permitiendo secciones de código independientes para registrar sus clases) sería map<name, factory function pointer>.

+1

"Una versión extendida de eso (permitiendo secciones de código independientes para registrar sus clases) sería un mapa ." ¿sería posible, a partir de una definición de clase, registrarse en ese mapa, suponiendo que este mapa está en una fábrica en otro lugar? En Java podría/haría usando constructores estáticos {}. De hecho, no quiero para cada nueva subclase que escribo modificar el código de la fábrica. – puccio

5

¿Por qué no utilizar una fábrica de objetos?

En su forma más simple:

BaseClass* myFactory(std::string const& classname, params...) 
{ 
    if(classname == "Class1"){ 
     return new Class1(params...); 
    }else if(...){ 
     return new ...; 
    }else{ 
     //Throw or return null 
    } 
    return NULL; 
} 
1

Menciona dos posibilidades: Línea de comando y macro de compilación, pero la solución para cada uno es muy diferente.

Si la elección se realiza mediante una macro de compilación que es un problema simple que se puede resolver con #defines y #ifdefs y similares. La solución que propones es tan buena como cualquiera.

Pero si la elección se realiza en tiempo de ejecución utilizando un argumento de línea de comando, entonces necesita tener algún marco de Fábrica que pueda recibir una cadena y crear el objeto apropiado. Esto se puede hacer usando una cadena if().. else if()... else if()... simple y estática que tiene todas las posibilidades o puede ser un marco completamente dinámico donde los objetos se registran y se clonan para proporcionar nuevas instancias de ellos mismos.

10

Una forma de implementar esto es codificando una asignación de nombres de clases a una función de fábrica. Las plantillas pueden acortar el código. El STL puede facilitar la codificación.

#include "BaseObject.h" 
#include "CommonClasses.h" 

template< typename T > BaseObject* fCreate(int param1, bool param2) { 
    return new T(param1, param2); 
} 

typedef BaseObject* (*tConstructor)(int param1, bool param2); 
struct Mapping { string classname; tConstructor constructor; 
    pair<string,tConstructor> makepair()const { 
     return make_pair(classname, constructor); 
    } 
} mapping[] = 
{ { "class1", &fCreate<Class1> } 
, { "class2", &fCreate<Class2> } 
// , ... 
}; 

map< string, constructor > constructors; 
transform(mapping, mapping+_countof(mapping), 
    inserter(constructors, constructors.begin()), 
    mem_fun_ref(&Mapping::makepair)); 

EDITAR - bajo petición en general :) un poco de la reanudación de hacer las cosas se ven más suaves (créditos a Piedra Libre que probablemente no quieren añadir una respuesta a sí mismo)

typedef BaseObject* (*tConstructor)(int param1, bool param2); 
struct Mapping { 
    string classname; 
    tConstructor constructor; 

    operator pair<string,tConstructor>() const { 
     return make_pair(classname, constructor); 
    } 
} mapping[] = 
{ { "class1", &fCreate<Class1> } 
, { "class2", &fCreate<Class2> } 
// , ... 
}; 

static const map< string, constructor > constructors( 
     begin(mapping), end(mapping)); // added a flavor of C++0x, too. 
+0

Yeargs, la llamada 'transformar' al final me daña los ojos. % -) –

+0

Tienes razón. También puede _no_ usar un mapa y encontrar el constructor adecuado con un bucle. – xtofl

+0

¡Realmente no está muy lejos de tener las funciones necesarias en su ejemplo anterior para hacer exactamente lo mismo, pero sin todo lo que transforma la basura! Dentro de tu estructura de mapeo tienes una función de utilidad make_pair. Estás pensando en las líneas correctas. El constructor del mapa toma punteros como argumentos (de hecho no están tipeados) Puede aprovechar esto. Lo mejor es tener sus datos estáticos usando tipos integrales, por lo que debe cambiar los dos miembros de cadena para const char * s. Luego cambie su función make_pair en un operador mapped_type –

0

En el pasado , He implementado el patrón Factory de tal manera que las clases pueden autoregistrarse en tiempo de ejecución sin que la fábrica tenga que saber específicamente sobre ellas. La clave es utilizar una característica de compilador no estándar llamada (IIRC) "archivo adjunto por inicialización", en la que declara una variable estática ficticia en el archivo de implementación para cada clase (por ejemplo, un bool) y la inicializa con una llamada al registro rutina.

En este esquema, cada clase tiene que #incluir el encabezado que contiene su fábrica, pero la fábrica no conoce nada excepto la clase de interfaz. Literalmente puede agregar o eliminar clases de implementación de su compilación y volver a compilar sin cambios de código.

El problema es que sólo algunos compiladores soportar la unión de inicialización - IIRC otros inicializar las variables de archivo de alcance en el primer uso (de la misma forma-función local de trabajo estática), que no es de ayuda aquí ya que la variable dummy nunca se accede y se el mapa de la fábrica siempre se encontrará vacío.

Los compiladores que me interesan (MSVC y GCC) sí lo permiten, así que no es un problema para mí. Deberá decidir por sí mismo si esta solución le conviene.

1

Aunque la pregunta existe ahora por más de cuatro años, sigue siendo útil.Porque pedir un nuevo código desconocido al momento de compilar y vincular los archivos de código principales es en estos días un escenario muy común. Una solución a esta pregunta no se menciona en absoluto. Por lo tanto, me gusta señalar a la audiencia a un tipo diferente de solución no construida en C++. C++ en sí no tiene la capacidad de comportarse como Class.forName() conocido de Java o como Activator.CreateInstance(type) conocido de .NET. Debido a razones mencionadas, que no hay supervisión por parte de una máquina virtual para el código JIT sobre la marcha. Pero de todos modos, LLVM, la máquina virtual de bajo nivel, le brinda las herramientas y libs necesarios para leer en una lib compilada. Básicamente, debe ejecutar dos pasos:

  1. compile su código fuente C/C++, que le gustaría crear una instancia de forma dinámica. Necesitas compilarlo en código de bits, por lo que terminas en, digamos, foo.bc. Puede hacerlo con sonido metálico y proporcionar un modificador de compilador: clang -emit-llvm -o foo.bc -c foo.c
  2. Es necesario entonces utilizar el método ParseIRFile() de llvm/IRReader/IRReader.h para analizar el archivo foo.bc para obtener las funciones relevantes (sí LLVM sólo sabe funciones como el código binario es una abstracción directa de Los códigos de operación de la CPU son bastante poco similares a las representaciones intermedias de alto nivel como el bytecode de Java. Consulte, por ejemplo, este article para obtener una descripción del código más completa.

Después de configurar estos pasos descritos anteriormente, puede llamar dinámicamente también desde C++ otras funciones y métodos desconocidos anteriores.

Cuestiones relacionadas