¿Cuáles son algunos usos de los parámetros de plantilla de plantilla?


238

He visto algunos ejemplos de C ++ usando 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?


44
Vine desde la otra dirección (FP, Haskell, etc.) y aterricé en esto: stackoverflow.com/questions/2565097/higher-kinded-types-with-c
Erik Kaplun

Respuestas:


197

Creo que debe usar la sintaxis de plantilla de plantilla para pasar un parámetro cuyo tipo es una plantilla dependiente de otra plantilla como esta:

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

Aquí Hhay una plantilla, pero quería que esta función se ocupe de todas las especializaciones de H.

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

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

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

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::vectortiene dos parámetros de plantilla, tipo y asignador, por lo que tuvimos que aceptar ambos. Afortunadamente, debido a la deducción de tipo, no necesitaremos escribir el tipo exacto explícitamente.

que puedes usar así:

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

o mejor aún, podemos usar:

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

ACTUALIZACIÓN : Incluso este ejemplo artificial, aunque ilustrativo, ya no es un ejemplo sorprendente debido a la introducción de c ++ 11 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;
}

así es como preferiría 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 <T> como argumento. Hubiera esperado que la versión sin el argumento std :: allocator hubiera funcionado utilizando el parámetro predeterminado de std :: vector. ¿Hay alguna actualización en este wrt C ++ 0x?
Amit

Bueno, no tienes que proporcionar el asignador. Lo importante es que el parámetro de plantilla de plantilla se definió sobre el número correcto de argumentos. Pero la función no debería importarle cuáles son sus "tipos" o significado, ya que funciona bien en C ++ 98:template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
pfalcon

Me pregunto por qué la instanciación es f<vector,int>y no f<vector<int>>.
bobobobo

2
@bobobobo Estos dos significan cosas diferentes. f<vector,int>significa f<ATemplate,AType>, f<vector<int>>significaf<AType>
usuario362515

@phaedrus: (mucho más tarde ...) puntos buenos, mejoró el ejemplo para hacer que el asignador sea genérico y el ejemplo más claro :-)
Evan Teran

163

En realidad, el caso de uso para los parámetros de plantilla de plantilla es bastante obvio. Una vez que sepa que C ++ stdlib tiene un gran vacío de no definir operadores de salida de flujo para tipos de contenedor estándar, procederá 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 descubrirías que el código para el vector es el mismo, para forward_list es el mismo, en realidad, incluso para multitud de tipos de mapas sigue siendo el mismo. Esas clases de plantilla no tienen nada en común, excepto la metainterfaz / protocolo, y el uso de parámetros de plantilla de plantilla permite capturar la comunidad 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 el asignador está predeterminado, aún debemos tener en cuenta 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 funcionará automáticamente para todos los contenedores de secuencia presentes y futuros que se adhieran al protocolo estándar. Para agregar mapas a la mezcla, sería necesario echar un vistazo a la referencia para notar que aceptan 4 parámetros de plantilla, por lo que necesitaríamos otra versión del operador << anterior con el parámetro de plantilla de plantilla de 4 argumentos. También veríamos que std: pair intenta representarse con el operador 2-arg << para los tipos de secuencia que definimos anteriormente, por lo que proporcionaríamos una especialización solo para std :: pair.

Por cierto, con C + 11 que permite plantillas variadas (y, por lo tanto, debería permitir argumentos de plantilla de plantilla variable), sería posible tener un solo operador << para gobernarlos a todos. 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 

99
Este es un ejemplo tan dulce de parámetros de plantilla de plantilla, ya que muestra un caso con el que todos han tenido que lidiar.
Ravenwater

3
Esta es la respuesta más despertadora para mí en las plantillas de C ++. @WhozCraig ¿Cómo obtuvo los detalles de expansión de la plantilla?
Arun

3
@Arun gcc admite una macro llamada __PRETTY_FUNCTION__, que, entre otras cosas, informa las descripciones de los parámetros de la plantilla en texto plano. el sonido metálico también lo hace. Una característica más útil a veces (como puede ver).
WhozCraig

20
El parámetro de plantilla de plantilla aquí no agrega realmente ningún valor. También podría usar un parámetro de plantilla regular como cualquier instancia dada de una plantilla de clase.
David Stone

99
Tengo que estar de acuerdo con David Stone. No tiene sentido el parámetro de plantilla de plantilla aquí. Sería mucho más simple e igualmente efectivo hacer una plantilla simple (template <typename Container>). Sé que esta publicación es bastante antigua, por lo que solo agrego mis 2 centavos para las personas que se encuentran con esta respuesta en busca de información sobre plantillas de plantillas.
Jim Vargo

67

Aquí hay un ejemplo simple tomado de 'Diseño moderno de C ++ - Programación genérica y patrones de diseño aplicados' por Andrei Alexandrescu:

Utiliza clases con parámetros de plantilla de plantilla para implementar el patrón de política:

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

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

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

typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;

En lugar de la forma más engorrosa y propensa a errores que una definición que carece de argumentos de plantilla de plantilla habría requerido:

typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;

1
La pregunta solicitó específicamente ejemplos distintos al patrón de política.
user2913094

Llegué a esta pregunta exactamente de este libro. Una nota digna es que los parámetros de la plantilla de plantilla también aparecen en el capítulo Lista de tipos y en el capítulo Generación de clase con listas de tipos .
Victor

18

Aquí hay otro ejemplo práctico de mi biblioteca de redes neuronales convolucionales CUDA . Tengo la siguiente plantilla de clase:

template <class T> class Tensor

que en realidad implementa la manipulación de matrices n-dimensionales. También hay una plantilla de clase secundaria:

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

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

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 aquí para tener una sintaxis de plantilla es porque puedo declarar la implementación de la clase

class CLayerCuda: public CLayerT<TensorGPU, float>

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


¿Puede forzar la deducción de T con algo como: "plantilla <clase T, plantilla <T> TT> CLayerT" y "clase CLayerCuda: public CLayerT <TensorGPU <float>>"? En caso de que no necesite un TT <otherT>
NicoBerrogorry

NUNCA MENTE: plantilla <plantilla <clase T> clase U> clase B1 {}; de ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/… de una búsqueda rápida en Google
NicoBerrogorry

12

Supongamos que está utilizando CRTP para proporcionar una "interfaz" para un conjunto de plantillas secundarias; y tanto el padre como el hijo son paramétricos en otros argumentos de 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;

Tenga en cuenta la duplicación de 'int', que en realidad es el mismo parámetro de tipo especificado para ambas plantillas. Puede usar una plantilla de 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 está eliminando proporcionar directamente los otros parámetros de plantilla a la plantilla derivada ; la "interfaz" todavía los recibe.

Esto también le permite construir typedefs 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 typedef en una plantilla no especificada. Sin embargo, esto funciona (y C ++ 11 tiene soporte nativo para plantillas typedefs):

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

typedef typename derived_interface_type<int>::type derived_t;

Desafortunadamente, necesita un derivado_tipo_interfaz_por cada instancia de la plantilla derivada, a menos que haya otro truco que aún no haya aprendido.


Necesitaba esta solución exacta para algún código (¡gracias!). Aunque funciona, no entiendo cómo derivedse puede usar la clase de plantilla sin sus argumentos de plantilla, es decir, la líneatypedef typename interface<derived, VALUE> type;
Carlton

@Carlton funciona básicamente porque el parámetro de plantilla correspondiente que se llena se define como a template <typename>. En cierto sentido, puede pensar que los parámetros de la plantilla tienen un 'metatipo'; el metatipo normal para un parámetro de plantilla es lo typenameque significa que debe llenarse con un tipo regular; el templatemetatipo significa que debe llenarse con una referencia a una plantilla. deriveddefine una plantilla que acepta un typenameparámetro metatipado, por lo que se ajusta a la factura y se puede hacer referencia aquí. ¿Tener sentido?
Mark McKenna

C ++ 11 todavía todavía typedef. Además, puede evitar el duplicado inten su primer ejemplo utilizando una construcción estándar como a value_typeen el tipo DERIVADO.
rubenvb

Esta respuesta en realidad no se dirige a C ++ 11; Hice referencia a C ++ 11 solo para decir que puede typedefsolucionar el problema desde el bloque 2. Pero creo que el punto 2 es válido ... sí, esa sería probablemente una forma más sencilla de hacer lo mismo.
Mark McKenna

7

Esto es con 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();
}

4

En la solución con plantillas variadas proporcionadas por pfalcon, me resultó difícil especializar el operador ostream para std :: map debido a la naturaleza codiciosa de la especialización variable. Aquí hay una pequeña 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 uno generalizado de algo que acabo de usar. Lo estoy publicando ya que es un ejemplo muy simple y demuestra un caso de uso práctico junto con argumentos predeterminados:

#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;
};

2

Mejora la legibilidad de su código, proporciona seguridad de tipo adicional y ahorra algunos esfuerzos del compilador.

Digamos que desea imprimir cada elemento de un contenedor, puede usar el siguiente código sin parámetro de plantilla de plantilla

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

o con el parámetro de plantilla de 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';
}

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

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


1

Lo uso para tipos versionados.

Si tiene un tipo versionado a través de una plantilla como MyType<version>, puede escribir una función en la que pueda capturar el número de versión:

template<template<uint8_t> T, uint8_t Version>
Foo(const T<Version>& obj)
{
    assert(Version > 2 && "Versions older than 2 are no longer handled");
    ...
    switch (Version)
    {
    ...
    }
}

Por lo tanto, puede hacer diferentes cosas dependiendo de la versión del tipo que se pasa en lugar de tener una sobrecarga para cada tipo. También puede tener funciones de conversión que aceptan MyType<Version>y devuelven MyType<Version+1>, de forma genérica, e incluso las repiten para tener una ToNewest()función que devuelve la última versión de un tipo de cualquier versión anterior (muy útil para registros que podrían haberse almacenado hace un tiempo pero debe procesarse con la herramienta más nueva de hoy).

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.