2008-10-17 11 views
167

He visto algunos ejemplos de C++ utilizando los parámetros de plantilla de plantilla (es decir, plantillas que toman plantillas como parámetros) para hacer un diseño de clase basado en políticas. ¿Qué otros usos tiene esta técnica?¿Cuáles son algunos usos de los parámetros de plantilla de plantilla en C++?

+4

Vine desde la otra dirección (FP, Haskell, etc.) y aterrizó en esto: http://stackoverflow.com/questions/2565097/higher-kinded-types-with-c –

Respuesta

138

Creo que se necesita para utilizar sintaxis de la plantilla plantilla para pasar un parámetro cuyo tipo es dependiente de una plantilla en otra plantilla como esta:

template <template<class> class H, class S> 
void f(const H<S> &value) { 
} 

Aquí, H es una plantilla, pero quería esta función para hacer frente con todas las especializaciones de H.

NOTA: He estado programando C++ durante muchos años y solo lo he necesitado una vez. Me parece que es una característica que rara vez se necesita (¡por supuesto, a mano cuando la necesitas!).

He estado tratando de pensar en buenos ejemplos, y para ser honesto, la mayoría de las veces esto no es necesario, pero vamos a dar un ejemplo. Supongamos que std::vectorno tiene tiene un typedef value_type.

Entonces, ¿cómo escribirías una función que puede crear variables del tipo correcto para los elementos de vectores? Esto funcionaría

template <template<class, class> class V, class T, class A> 
void f(V<T, A> &v) { 
    // This can be "typename V<T, A>::value_type", 
    // but we are pretending we don't have it 

    T temp = v.back(); 
    v.pop_back(); 
    // Do some work on temp 

    std::cout << temp << std::endl; 
} 

NOTA: std::vector que tiene dos parámetros de plantilla, tipo y asignador, así que tuvimos que aceptar los dos. Afortunadamente, debido a la deducción de tipo, no necesitaremos escribir el tipo exacto explícitamente.

que se puede utilizar de esta manera:

f<std::vector, int>(v); // v is of type std::vector<int> using any allocator 

o mejor aún, que sólo puede utilizar:

f(v); // everything is deduced, f can deal with a vector of any type! 

ACTUALIZACIÓN: Incluso este ejemplo artificial, mientras ilustrativo, ya una no está ejemplo sorprendente debido a C++ 11 introduciendo auto. Ahora la misma función se puede escribir como:

template <class Cont> 
void f(Cont &v) { 

    auto temp = v.back(); 
    v.pop_back(); 
    // Do some work on temp 

    std::cout << temp << std::endl; 
} 

que es cómo prefiero escribir este tipo de código.

+1

Si f es una función definida por el usuario de una biblioteca, es feo que el usuario necesite pasar std :: allocator como argumento. Hubiera esperado que la versión sin el argumento std :: allocator hubiera funcionado usando el parámetro predeterminado de std :: vector. ¿Hay actualizaciones en este wrt C++ 0x? –

+0

Bueno, no es necesario que proporcione un asignador. Lo que es importante es que el parámetro de plantilla de plantilla se definió sobre la cantidad correcta de argumentos. Pero la función no debería importar cuáles son sus "tipos" o significado, siguiendo bien en C++ 98: 'plantilla clase C, clase T, clase U> vacío f (C & v)' – pfalcon

+0

Me pregunto por qué la instanciación es 'f ' y no 'f >'. – bobobobo

53

Aquí está un ejemplo sencillo tomado de 'Modern C++ Design - Generic Programming and Design Patterns Applied' por Andrei Alexandrescu:

usa una clases con los parámetros de plantilla plantilla con el fin de implementar el patrón de política:

// Library code 
template <template <class> class CreationPolicy> 
class WidgetManager : public CreationPolicy<Widget> 
{ 
    ... 
}; 

Explica: Por lo general, la clase de host ya sabe, o puede deducir fácilmente, el argumento de plantilla de la clase de política. En el ejemplo anterior, WidgetManager siempre maneja los objetos de tipo Widget, por lo que requerir que el usuario especifique Widget nuevamente en la instanciación de CreationPolicy es redundante y potencialmente peligroso. En este caso, el código de la biblioteca puede usar parámetros de plantilla para especificar políticas.

El efecto es que el código de cliente puede utilizar 'WidgetManager' de una manera más elegante:

typedef WidgetManager<MyCreationPolicy> MyWidgetMgr; 

En lugar de la forma propensa más engorroso, y el error de que una definición carente de argumentos de plantilla plantilla habría requerido :

typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr; 
+1

La pregunta solicitada específicamente para ejemplos distintos del patrón de política. – user2913094

+1

Me alegro de que haya incluido su respuesta de todos modos. Exactamente lo que necesitaba: D – Mordachai

18

Aquí hay otro ejemplo práctico de mi CUDA Convolutional neural network library. I tienen la siguiente plantilla de clase:

template <class T> class Tensor 

que es en realidad implementa manipulación matrices n-dimensional. También hay una plantilla de clase hija:

template <class T> class TensorGPU : public Tensor<T> 

que implementa la misma funcionalidad pero en la GPU. Ambas plantillas se pueden trabajar con todos los tipos básicos, como float, double, int, etc Y también tengo una plantilla de clase (simplificado):

template <template <class> class TT, class T> class CLayerT: public Layer<TT<T> > 
{ 
    TT<T> weights; 
    TT<T> inputs; 
    TT<int> connection_matrix; 
} 

La razón para tener sintaxis de la plantilla de la plantilla es porque puedo declarar implementación de la clase

class CLayerCuda: public CLayerT<TensorGPU, float> 

que tendrá a pesos y entradas de tipo de flotador y en GPU, pero connection_matrix siempre será int, ya sea en la CPU (especificando TT = Tensor) o en GPU (especificando TT = TensorGPU).

10

Supongamos que está utilizando CRTP para proporcionar una "interfaz" para un conjunto de plantillas secundarias; y tanto el padre y el niño son paramétricas en otro argumento (s) plantilla:

template <typename DERIVED, typename VALUE> class interface { 
    void do_something(VALUE v) { 
     static_cast<DERIVED*>(this)->do_something(v); 
    } 
}; 

template <typename VALUE> class derived : public interface<derived, VALUE> { 
    void do_something(VALUE v) { ... } 
}; 

typedef interface<derived<int>, int> derived_t; 

Nota la duplicación de 'int', que en realidad es el mismo parámetro tipo especificado a ambas plantillas. Puede utilizar una plantilla plantilla para DERIVADO para evitar esta duplicación:

template <template <typename> class DERIVED, typename VALUE> class interface { 
    void do_something(VALUE v) { 
     static_cast<DERIVED<VALUE>*>(this)->do_something(v); 
    } 
}; 

template <typename VALUE> class derived : public interface<derived, VALUE> { 
    void do_something(VALUE v) { ... } 
}; 

typedef interface<derived, int> derived_t; 

Tenga en cuenta que es la eliminación directa proporcionando el otro parámetro (s) plantilla a la deriva plantilla; la "interfaz" todavía los recibe.

Esto también le permite construir tiposdefs en la "interfaz" que dependen de los parámetros de tipo, a los que se podrá acceder desde la plantilla derivada.

El typedef anterior no funciona porque no puede escribir def a una plantilla no especificada. Esto funciona, sin embargo (y C++ 11 tiene soporte nativo para typedefs plantilla):

template <typename VALUE> 
struct derived_interface_type { 
    typedef typename interface<derived, VALUE> type; 
}; 

typedef typename derived_interface_type<int>::type derived_t; 

Es necesario un derived_interface_type para cada instanciación de la plantilla derivada por desgracia, a menos que haya otro truco que no he aprendido todavía.

+0

Necesitaba esta solución exacta para algunos códigos (¡gracias!). Aunque funciona, no entiendo cómo se puede usar la clase de plantilla 'derived' sin sus argumentos de plantilla, es decir, la línea' typedef typename interface type; ' – Carlton

+0

@Carlton funciona básicamente porque el parámetro de plantilla correspondiente ser llenado se define como una 'plantilla '. En cierto sentido, puede pensar que los parámetros de plantilla tienen un 'metatipo'; el metatipo normal para un parámetro de plantilla es 'typename', lo que significa que debe llenarse con un tipo regular; el metatótopo 'plantilla' significa que debe llenarse con una referencia a una plantilla. 'derived' define una plantilla que acepta un parámetro metapuesto' typename', por lo que se ajusta a la factura y se puede hacer referencia aquí. ¿Tener sentido? –

119

En realidad, el uso de la plantilla para los parámetros de la plantilla es bastante obvio.Una vez que se entera de que stdlib C++ ha enorme agujero de no definir los operadores de salida de corriente de los tipos de contenedores estándar, se procedería a escribir algo como:

template<typename T> 
static inline std::ostream& operator<<(std::ostream& out, std::list<T> const& v) 
{ 
    out << '['; 
    if (!v.empty()) { 
     for (typename std::list<T>::const_iterator i = v.begin(); ;) { 
      out << *i; 
      if (++i == v.end()) 
       break; 
      out << ", "; 
     } 
    } 
    out << ']'; 
    return out; 
} 

Entonces será averiguar que el código para el vector es lo mismo, para forward_list es lo mismo, en realidad, incluso para una gran cantidad de tipos de mapas, sigue siendo el mismo. Esas clases de plantilla no tienen nada en común a excepción de la meta-interfaz/protocolo, y el uso de un parámetro de plantilla de plantilla permite capturar las características comunes en todos ellos. Sin embargo, antes de proceder a escribir una plantilla, vale la pena verificar una referencia para recordar que los contenedores de secuencia aceptan 2 argumentos de plantilla: para el tipo de valor y el asignador. Si bien se predetermina asignador, aún debemos dar cuenta de su existencia en nuestro operador de plantilla < <:

template<template <typename, typename> class Container, class V, class A> 
std::ostream& operator<<(std::ostream& out, Container<V, A> const& v) 
... 

Voila, que trabajará para que automaticamente todos los presentes y futuros contenedores de secuencias que se adhieren al protocolo estándar. Para agregar mapas a la mezcla, se echa un vistazo a la referencia para señalar que aceptan 4 parámetros de plantilla, por lo que necesitaríamos otra versión del operador < < anterior con el modelo de plantilla de plantilla 4-arg. También veríamos que std: pair intenta representarse con el operador 2-arg < < para los tipos de secuencia que definimos previamente, por lo que proporcionaríamos una especialización solo para std :: pair.

Por cierto, con C + 11 que permite plantillas variadic (y por lo tanto debería permitir args de plantilla de plantilla variadica), sería posible tener un solo operador < < para gobernarlas todas. Por ejemplo:

#include <iostream> 
#include <vector> 
#include <deque> 
#include <list> 

template<typename T, template<class,class...> class C, class... Args> 
std::ostream& operator <<(std::ostream& os, const C<T,Args...>& objs) 
{ 
    os << __PRETTY_FUNCTION__ << '\n'; 
    for (auto const& obj : objs) 
     os << obj << ' '; 
    return os; 
} 

int main() 
{ 
    std::vector<float> vf { 1.1, 2.2, 3.3, 4.4 }; 
    std::cout << vf << '\n'; 

    std::list<char> lc { 'a', 'b', 'c', 'd' }; 
    std::cout << lc << '\n'; 

    std::deque<int> di { 1, 2, 3, 4 }; 
    std::cout << di << '\n'; 

    return 0; 
} 

salida

std::ostream &operator<<(std::ostream &, const C<T, Args...> &) [T = float, C = vector, Args = <std::__1::allocator<float>>] 
1.1 2.2 3.3 4.4 
std::ostream &operator<<(std::ostream &, const C<T, Args...> &) [T = char, C = list, Args = <std::__1::allocator<char>>] 
a b c d 
std::ostream &operator<<(std::ostream &, const C<T, Args...> &) [T = int, C = deque, Args = <std::__1::allocator<int>>] 
1 2 3 4 
+5

Este es un ejemplo tan dulce de parámetros de plantilla de plantilla, ya que muestra un caso que todos han tenido que enfrentar. – Ravenwater

+9

+1, se agregó un ejemplo de C++ 11 utilizando plantillas variadic. – WhozCraig

+3

Esta es la respuesta que más me ha despertado en las plantillas de C++. @WhozCraig ¿Cómo obtuvo los detalles de expansión de la plantilla? – Arun

4

Esto es lo que me encontré:

template<class A> 
class B 
{ 
    A& a; 
}; 

template<class B> 
class A 
{ 
    B b; 
}; 

class AInstance : A<B<A<B<A<B<A<B<... (oh oh)>>>>>>>> 
{ 

}; 

se puede resolver a:

template<class A> 
class B 
{ 
    A& a; 
}; 

template< template<class> class B> 
class A 
{ 
    B<A> b; 
}; 

class AInstance : A<B> //happy 
{ 

}; 

o (código de trabajo):

template<class A> 
class B 
{ 
public: 
    A* a; 
    int GetInt() { return a->dummy; } 
}; 

template< template<class> class B> 
class A 
{ 
public: 
    A() : dummy(3) { b.a = this; } 
    B<A> b; 
    int dummy; 
}; 

class AInstance : public A<B> //happy 
{ 
public: 
    void Print() { std::cout << b.GetInt(); } 
}; 

int main() 
{ 
    std::cout << "hello"; 
    AInstance test; 
    test.Print(); 
} 
3

En la solución con las plantillas proporcionadas por variadic pfalcon, me resultaba difícil especializarse en realidad el operador ostream para std :: mapa debido a la naturaleza codiciosa de la especialización variadic. Aquí hay una ligera revisión que funcionó para mí:

#include <iostream> 
#include <vector> 
#include <deque> 
#include <list> 
#include <map> 

namespace containerdisplay 
{ 
    template<typename T, template<class,class...> class C, class... Args> 
    std::ostream& operator <<(std::ostream& os, const C<T,Args...>& objs) 
    { 
    std::cout << __PRETTY_FUNCTION__ << '\n'; 
    for (auto const& obj : objs) 
     os << obj << ' '; 
    return os; 
    } 
} 

template< typename K, typename V> 
std::ostream& operator << (std::ostream& os, 
       const std::map< K, V > & objs) 
{ 

    std::cout << __PRETTY_FUNCTION__ << '\n'; 
    for(auto& obj : objs) 
    {  
    os << obj.first << ": " << obj.second << std::endl; 
    } 

    return os; 
} 


int main() 
{ 

    { 
    using namespace containerdisplay; 
    std::vector<float> vf { 1.1, 2.2, 3.3, 4.4 }; 
    std::cout << vf << '\n'; 

    std::list<char> lc { 'a', 'b', 'c', 'd' }; 
    std::cout << lc << '\n'; 

    std::deque<int> di { 1, 2, 3, 4 }; 
    std::cout << di << '\n'; 
    } 

    std::map< std::string, std::string > m1 
    { 
     { "foo", "bar" }, 
     { "baz", "boo" } 
    }; 

    std::cout << m1 << std::endl; 

    return 0; 
} 
2

Aquí hay una generalización de algo que acabo de utilizar. He publicado ya que es un ejemplo sencillo muy, y demuestra un caso práctico su uso junto con los argumentos por defecto:

#include <vector> 

template <class T> class Alloc final { /*...*/ }; 

template <template <class T> class allocator=Alloc> class MyClass final { 
    public: 
    std::vector<short,allocator<short>> field0; 
    std::vector<float,allocator<float>> field1; 
}; 
0

Mejora la legibilidad del código, proporciona seguridad de tipos adicional y guardar algunos esfuerzos del compilador.

decir que quiere imprimir cada elemento de un contenedor, puede utilizar el siguiente código y sin parámetro de plantilla plantilla

template <typename T> void print_container(const T& c) 
{ 
    for (const auto& v : c) 
    { 
     std::cout << v << ' '; 
    } 
    std::cout << '\n'; 
} 

o con parámetro de plantilla plantilla

template< template<typename, typename> class ContainerType, typename ValueType, typename AllocType> 
void print_container(const ContainerType<ValueType, AllocType>& c) 
{ 
    for (const auto& v : c) 
    { 
     std::cout << v << ' '; 
    } 
    std::cout << '\n'; 
} 

Supongamos que se pasa en un entero decir print_container(3). Para el primer caso, la plantilla será instanciada por el compilador que se quejará sobre el uso de c en el ciclo for, este último no instanciará la plantilla en absoluto ya que no se puede encontrar ningún tipo de coincidencia.

En general, si su clase/función de plantilla está diseñada para manejar la clase de plantilla como parámetro de plantilla, es mejor dejarlo en claro.

Cuestiones relacionadas