2010-03-12 5 views
5

Esto se simplifica por el simple hecho de la pregunta. Digamos que tiene una jerarquía:Emular el envío dinámico en C++ en función de los parámetros de plantilla

struct Base { 
    virtual int precision() const = 0; 
}; 

template<int Precision> 
struct Derived : public Base { 

    typedef Traits<Precision>::Type Type; 

    Derived(Type data) : value(data) {} 
    virtual int precision() const { return Precision; } 

    Type value; 

}; 

Quiero un función no molde con la firma:

Base* function(const Base& a, const Base& b); 

Cuando el tipo específico del resultado de la función es el mismo tipo que el que sea de a y b tiene el mayor Precision; algo como el siguiente pseudocódigo:

Base* function(const Base& a, const Base& b) { 

    if (a.precision() > b.precision()) 

     return new A(((A&)a).value + A(b.value).value); 

    else if (a.precision() < b.precision()) 

     return new B(B(((A&)a).value).value + ((B&)b).value); 

    else 

     return new A(((A&)a).value + ((A&)b).value); 

} 

Dónde A y B son los tipos específicos de a y b, respectivamente. Quiero function para operar independientemente de la cantidad de instancias de Derived que hay. Me gustaría evitar una tabla masiva de comparaciones typeid(), aunque RTTI está bien en las respuestas. ¿Algunas ideas?

+2

Creo que debería mencionar que no conoce el tipo de clase completo. Simplemente conoces 'Base &'. Varias respuestas, incluida la mía, asumieron que usted conoce el tipo exacto 'Derived '. –

+0

Observando desde un comentario en una respuesta: Un requisito adicional es que la función no puede ser una plantilla; debe tener la firma Base * (Base &, Base &) dada. –

+0

Incorporado las restricciones más claramente en la pregunta. –

Respuesta

3

No puede hacer que function() delegue al código de plantilla directamente sin seleccionar entre una lista masiva de todos los tipos posibles, porque las plantillas se expanden en tiempo de compilación y en tiempo de compilación la función() no sabe qué deriva tipos con los que se llamará. Necesita tener instancias compiladas del código de plantilla para cada versión de su función de plantilla operation que será necesaria, que es potencialmente un conjunto infinito.

Siguiendo esa lógica, el único lugar que conoce todas las plantillas que pueden requerirse es la propia clase Derived.Por lo tanto, la clase Derived debe incluir un miembro de:

Derived<Precision> *operation(Base& arg2) { 
    Derived<Precision> *ptr = new Derived<Precision>; 
    // ... 
    return ptr; 
} 

A continuación, puede definir function como tal, y hacer el envío indirectamente:

Base* function(const Base& a, const Base& b) { 
    if (a.precision() > b.precision()) 
    return a.operation(b); 
    else 
    return b.operation(a); 
} 

Tenga en cuenta que esta es la versión simplificada; si su operación no es simétrica en sus argumentos, deberá definir dos versiones de la función miembro: una con this en lugar del primer argumento, y otra con ella en lugar del segundo argumento.

Además, esto ha ignorado el hecho de que necesita alguna forma de a.operation para obtener una forma apropiada de b.value sin conocer el tipo derivado de b. Tendrás que resolverlo tú mismo: ten en cuenta que (por la misma lógica que antes) es imposible resolver esto al crear plantillas con el tipo de b, porque estás distribuyendo en tiempo de ejecución. La solución depende exactamente de qué tipos tenga, y de si hay algún modo de que un tipo de precisión más alta extraiga un valor de un objeto de precisión igual o inferior Derived sin conocer el tipo exacto de ese objeto. Esto puede no ser posible, en cuyo caso tienes la larga lista de coincidencias en los ID de tipo.

Usted no tiene que hacer eso en una sentencia switch, sin embargo. Puede asignar a cada Derived un conjunto de funciones de miembro para el up-casting a una función de mayor precisión. Por ejemplo:

template<int i> 
upCast<Derived<i> >() { 
    return /* upcasted value of this */ 
} 

Entonces, la función operator miembro puede operar en b.upcast<typeof(this)>, y no tener que hacer explícita la fundición para obtener un valor del tipo que necesita. Es posible que tenga que crear una instancia explícita de algunas de estas funciones para que se compilen; No he trabajado lo suficiente con RTTI para decirlo con certeza.

Aunque, básicamente, el problema es que si tiene N precisiones posibles, tiene N N combinaciones posibles, y cada una de estas tendrá que tener un código compilado por separado. Si no puede utilizar las plantillas en su definición de function, entonces usted tiene que tener versiones compiladas de todos los N N de estas posibilidades, y de alguna manera hay que indicar al compilador para generar todos ellos, y de alguna manera hay que recoger la derecha uno para enviar a en tiempo de ejecución. El truco de usar una función miembro elimina uno de esos factores de N, pero el otro permanece, y no hay forma de hacerlo completamente genérico.

+0

Esto es lo suficientemente bueno para mí.El segundo factor se elimina por el hecho de que 'Base' define una facilidad para convertir en tiempo de ejecución a un tipo arbitrario. –

+0

¡Oh, bien! Estaba editando para sugerir que tal instalación sería útil para eliminar el segundo factor. –

1

En primer lugar, desea hacer que su miembro precision tenga un valor de static const int, no una función, para que pueda operarlo en tiempo de compilación. En Derived, sería:

static const int precision = Precision; 

Entonces, es necesario algunas estructuras de ayuda para determinar la Base/clase derivada más-precisa. En primer lugar, se necesita una estructura ayudante-helper genérico para escoger uno de dos tipos dependiendo de un valor lógico:

template<typename T1, typename T2, bool use_first> 
struct pickType { 
    typedef T2 type; 
}; 

template<typename T1, typename T2> 
struct pickType<T1, T2, true> { 
    typedef T1 type; 
}; 

Entonces, pickType<T1, T2, use_first>::type resolverá a T1 si es use_firsttrue, y por otra parte a T2. Por lo tanto, a continuación, utilizamos esta escoger el tipo más precisa:

template<typename T1, typename T2> 
struct mostPrecise{ 
    typedef pickType<T1, T2, (T1::precision > T2::precision)>::type type; 
}; 

Ahora, mostPrecise<T1, T2>::type le dará cualquiera de los dos tipos tiene un mayor valor precision. Por lo tanto, puede definir su función como:

template<typename T1, typename T2> 
mostPrecise<T1, T2>::type function(const T1& a, const T2& b) { 
    // ... 
} 

Y ahí lo tiene.

+0

Esto es muy interesante y útil para otro proyecto mío, pero específicamente necesito que la decisión se tome en tiempo de ejecución. –

+0

Gracias. Dejaré este para un valor histórico en lugar de eliminarlo, entonces, pero vea mi otra respuesta para comentarios sobre cómo hacer esto en tiempo de ejecución. –

4

Lo que está pidiendo que se llama multiple dispatch, también conocido como métodos múltiples. No es una característica del lenguaje C++.

hay soluciones para casos especiales, pero no se puede evitar hacer alguna aplicación a sí mismo.

Un patrón común para el envío múltiple se llama "reexpedición", también conocido como "despacho diferida recursiva". Básicamente, un método virtual resuelve un tipo de parámetros y luego llama a otro método virtual, hasta que se resuelvan todos los parámetros. La función del exterior (si hay una) simplemente llama al primero de estos métodos virtuales.

Suponiendo que hay n clases derivadas, habrá un método para resolver el primer parámetro, n para resolver el segundo, n * n para resolver el tercero y así sucesivamente, en el peor de todos modos. Esa es una buena cantidad de trabajo manual, y el uso de bloques condicionales basados ​​en typeid podría ser más fácil para el desarrollo inicial, pero es más robusto para el mantenimiento para usar el redispatch.

class Base; 
class Derived1; 
class Derived2; 

class Base 
{ 
    public: 
    virtual void Handle (Base* p2); 

    virtual void Handle (Derived1* p1); 
    virtual void Handle (Derived2* p1); 
}; 

class Derived1 : public Base 
{ 
    public: 
    void Handle (Base* p2); 

    void Handle (Derived1* p1); 
    void Handle (Derived2* p1); 
}; 

void Derived1::Handle (Base* p2) 
{ 
    p2->Handle (this); 
} 

void Derived1::Handle (Derived1* p1) 
{ 
    // p1 is Derived1*, this (p2) is Derived1* 
} 

void Derived1::Handle (Derived2* p1) 
{ 
    // p1 is Derived2*, this (p2) is Derived1* 
} 

// etc 

La implementación de este uso de una plantilla para las clases derivadas sería difícil, y el metaprogramming plantilla para manejar probablemente sería ilegible, imposible de mantener y muy frágil. Sin embargo, implementar el envío utilizando métodos que no sean de plantilla, y luego usar una plantilla mixin (una clase de plantilla que toma su clase base como un parámetro de plantilla) para extender eso con características adicionales, puede no ser tan malo.

El visitor design pattern está estrechamente relacionado con (básicamente implementado con) redispatch IIRC.

El otro enfoque es utilizar un lenguaje diseñado para manejar el problema, y ​​hay algunas opciones que funcionan bien con C++. Una es usar treecc, un lenguaje específico de dominio para manejar nodos AST y operaciones de despacho múltiple que, como lex y yacc, genera el "código fuente" como salida.

Todo lo que hace para manejar las decisiones de despacho es generar instrucciones de conmutación basadas en una ID de nodo AST, que también puede ser una ID de clase de valor de tipo dinámico, IYSWIM. Sin embargo, estas son declaraciones de cambio que no tiene que escribir o mantener, que es una diferencia clave. El mayor problema que tengo es que los nodos AST tienen manipulado su manejo de destructor, lo que significa que los destructores de los datos de los miembros no reciben llamadas a menos que haga un esfuerzo especial, es decir, funciona mejor con los tipos de POD para los campos.

Otra opción es usar un preprocesador de lenguaje que admita multimétodos. Ha habido algunos de estos, en parte porque Stroustrup tenía ideas bastante bien desarrolladas para soportar multimétodos en un punto. CMM es uno. Doublecpp es otro. Otra más es el Frost Project. Creo que CMM está más cerca de lo que Stroustrup describió, pero no lo he comprobado.

En última instancia, sin embargo, el envío múltiple es solo una forma de tomar una decisión en tiempo de ejecución, y hay muchas maneras de manejar la misma decisión. Los DSL especializados traen una gran cantidad de molestias, por lo que generalmente solo lo hace si necesita una gran cantidad de despachos múltiples. El redispatch y el patrón de visitantes son robustos para el mantenimiento de WRT, pero a expensas de cierta complejidad y desorden. Los enunciados condicionales simples pueden ser una mejor opción para casos simples, aunque tenga en cuenta que detectar la posibilidad de un caso no controlado en tiempo de compilación es difícil, si no imposible.

Como suele ser el caso, no hay una sola manera correcta de hacerlo, al menos en C++.

Cuestiones relacionadas