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?
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?
Respuestas:
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í H
hay 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::vector
tiene 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.
template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
f<vector,int>
y no f<vector<int>>
.
f<vector,int>
significa f<ATemplate,AType>
, f<vector<int>>
significaf<AType>
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
__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).
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;
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).
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.
derived
se puede usar la clase de plantilla sin sus argumentos de plantilla, es decir, la líneatypedef typename interface<derived, VALUE> type;
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 typename
que significa que debe llenarse con un tipo regular; el template
metatipo significa que debe llenarse con una referencia a una plantilla. derived
define una plantilla que acepta un typename
parámetro metatipado, por lo que se ajusta a la factura y se puede hacer referencia aquí. ¿Tener sentido?
typedef
. Además, puede evitar el duplicado int
en su primer ejemplo utilizando una construcción estándar como a value_type
en el tipo DERIVADO.
typedef
solucionar 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.
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();
}
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;
}
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;
};
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.
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).