2009-03-06 14 views
7

¿Cuál es la relación entre el uso de funciones virtuales y los mecanismos de herencia C++ versus el uso de plantillas y algo así como conceptos de impulso?C++ verificación de concepto vs herencia

Parece que hay una gran coincidencia de lo que es posible. A saber, parece ser posible lograr un comportamiento polimórfico con cualquiera de los enfoques. Entonces, ¿cuándo tiene sentido favorecer a uno sobre el otro?

La razón por la que menciono esto es porque tengo un contenedor con plantillas, donde los contenedores en sí tienen una relación jerárquica. Me gustaría escribir algoritmos que usen estos contenedores sin importar qué contenedor específico es. Además, algunos algoritmos se beneficiarían de saber que el tipo de plantilla cumple ciertos conceptos (comparable, por ejemplo).

Entonces, por un lado, quiero que los contenedores se comporten polimórficamente. Por otro lado, aún tengo que usar conceptos si quiero implementar correctamente algunos algoritmos. ¿Qué debe hacer un desarrollador junior?

Respuesta

6

Creo que los conceptos son una especie de metainterfaz. Categorizan tipos según sus habilidades. La próxima versión de C++ proporciona conceptos nativos. No lo había entendido hasta que encontré los conceptos de C++ 1x y cómo permiten unir tipos diferentes pero no relacionados. Imagine que tiene una interfaz Range. Puedes modelar eso de dos maneras. Uno de ellos es una relación subtipo:

class Range { 
    virtual Iterator * begin() = 0; 
    virtual Iterator * end() = 0; 

    virtual size_t size() = 0; 
}; 

Por supuesto, cada clase que deriva de que implementa la interfaz rango y se puede utilizar con sus funciones. Pero ahora ves que es limitado. ¿Qué hay de una matriz? ¡Es un rango también!

T t[N]; 

begin() => t 
end() => t + size() 
size() => N 

Lamentablemente, no puede derivar una matriz de esa clase Range que implementa esa interfaz.Necesita un método adicional (sobrecarga). ¿Y qué hay de los contenedores de terceros? Un usuario de su biblioteca puede querer usar sus contenedores junto con sus funciones. Pero él no puede cambiar la definición de sus contenedores. Aquí, los conceptos entran en juego:

auto concept Range<typename T> { 
    typename iterator; 
    iterator T::begin(); 
    iterator T::end(); 
    size_t T::size(); 
} 

Ahora, usted dice algo sobre las operaciones compatibles de algún tipo que puede ser cumplida si T tiene las funciones miembro apropiadas. En tu biblioteca, escribirías la función genérica. Esto le permite aceptar cualquier tipo tanto tiempo como que soporta las operaciones necesarias:

template<Range R> 
void assign(R const& r) { 
    ... iterate from r.begin() to r.end(). 
} 

Es un gran tipo de sustitución . Cualquier tipo de se ajustará a la factura que se adhiere al concepto, y no solo a los tipos que implementan activamente alguna interfaz. El siguiente estándar de C++ va más allá: define un concepto Container que se ajustará a matrices simples (por algo llamado mapa conceptual que define cómo un tipo se ajusta a algún concepto) y otro, contenedores estándar existentes.

La razón por la que menciono esto es porque tengo un contenedor con plantilla, donde los contenedores en sí tienen una relación jerárquica. Me gustaría escribir algoritmos que usen estos contenedores sin importar qué contenedor específico es. Además, algunos algoritmos se beneficiarían de saber que el tipo de plantilla cumple ciertos conceptos (comparable, por ejemplo).

Puede hacer ambas cosas con las plantillas. Puede seguir teniendo su relación jerárquica para compartir código y luego escribir los algoritmos de forma genérica. Por ejemplo, para comunicar que su contenedor es comparable. Eso es como norma de acceso aleatorio/avance/salida/entrada categorías iterador se implementan:

// tag types for the comparator cagetory 
struct not_comparable { }; 
struct basic_comparable : not_comparable { }; 

template<typename T> 
class MyVector : public BasicContainer<T> { 
    typedef basic_comparable comparator_kind; 
}; 

/* Container concept */ 
T::comparator_kind: comparator category 

Es una manera sencilla razonable para hacerlo, en realidad. Ahora puede llamar a una función y reenviará a la implementación correcta.

template<typename Container> 
void takesAdvantage(Container const& c) { 
    takesAdvantageOfCompare(c, typename Container::comparator_kind()); 
} 

// implementation for basic_comparable containers 
template<typename Container> 
void takesAdvantage(Container const& c, basic_comparable) { 
    ... 
} 

// implementation for not_comparable containers 
template<typename Container> 
void takesAdvantage(Container const& c, not_comparable) { 
    ... 
} 

En realidad, existen diferentes técnicas que se pueden utilizar para implementar eso. Otra forma es usar boost::enable_if para habilitar o deshabilitar diferentes implementaciones cada vez.

+0

C++ 1x? ¿Significa eso que dejaron de publicar el nuevo estándar en esta década o están hablando del futuro desarrollo de C++? – jpalecek

+0

http://www.research.att.com/~bs/C++0xFAQ.html#concepts – jmucchiello

+0

jpalecek, quieren lanzarlo en 2010. Tengo la costumbre de llamarlo C++ 1x :) –

0

Si se puede tomar una decisión en tiempo de compilación, use plantillas. De lo contrario, use herencia y funciones virtuales.

1

Sí, el comportamiento polimórfico es posible con ambos mecanismos. De hecho, ambos son llamado polimorfismo también.

Las funciones virtuales le dan polimorfismo dinámico (porque se decide en tiempo de ejecución), mientras que las plantillas le dan polimorfismo estático (todo se decide en tiempo de compilación).

Y eso debería responder a la pregunta de cuál prefiere también. Siempre que sea posible, prefiera mover el trabajo al tiempo de compilación. Entonces, cuando pueda salirse con la suya, use plantillas para resolver sus necesidades de polimorfismo. Y cuando eso no es posible (debido a que necesita usar información del tipo de tiempo de ejecución, porque los tipos exactos no se conocen en tiempo de compilación), vuelva al polimorfismo dinámico.

(Por supuesto, puede haber otras razones para preferir una u otra. En particular, las plantillas requieren mover gran cantidad de código a archivos de encabezado que pueden o no ser un problema, y ​​la velocidad de compilación tiende a sufrir, que también puede o no ser un problema.)

0

En este caso específico se puede hacer algo como

template<typename T> 
class ContainerBase{}; 

template<typename T> 
class ContainerDerived : public ContainerBase<T> {}; 

Dado que cada tipo 'contenedor' es única para cada tipo de plantilla, no hay funciones miembro razón de cada tipo de contenedor no podría especializarse en los rasgos del tipo de plantilla.

0

Como un simple ejemplo de la diferencia entre el tiempo de compilación y tiempo de ejecución de polimorfismo en cuenta lo siguiente código:

template<typename tType> 
struct compileTimePolymorphism 
{ }; 

// compile time polymorphism, 
// you can describe a behavior on some object type 
// through the template, but you cannot interchange 
// the templates 
compileTimePolymorphism<int> l_intTemplate; 
compileTimePolymorphism<float> l_floatTemplate; 
compileTimePolymorphism *l_templatePointer; // ???? impossible 

struct A {}; 
struct B : public A{}; 
struct C : public A{}; 

// runtime polymorphism 
// you can interchange objects of different type 
// by treating them like the parent 
B l_B; 
C l_C: 
A *l_A = &l_B; 
l_A = &l_C; 

tiempo de compilación polimorfismo es una buena solución cuando el comportamiento de un objeto depende de alguna otra objeto. El polimorfismo en tiempo de ejecución es necesario donde el comportamiento de un objeto necesita ser cambiado.

Los dos se pueden combinar mediante la definición de una plantilla que es polimórfico:

template<typename tType> 
struct myContainer : public tType 
{}; 

La cuestión entonces es donde el comportamiento de su contenedor tiene que cambiar (polimorfismo de tiempo de ejecución), y donde el comportamiento depende de los objetos contiene (polimorfismo de tiempo de compilación).