c++ - template - ¿Cuáles son algunos usos de los parámetros de plantilla de plantilla?
typename c++ (9)
He visto algunos ejemplos de C ++ que utilizan parámetros de plantilla de plantilla (es decir, plantillas que toman plantillas como parámetros) para realizar un diseño de clase basado en políticas. ¿Qué otros usos tiene esta técnica?
Aquí hay otro ejemplo práctico de mi biblioteca de red neuronal convolucional CUDA . Tengo la siguiente plantilla de clase:
template <class T> class Tensor
que en realidad es implementa la manipulación de matrices n-dimensionales. También hay una plantilla de clase infantil:
template <class T> class TensorGPU : public Tensor<T>
Lo 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 para tener una plantilla de sintaxis 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 la CPU (especificando TT = Tensor) o en GPU (especificando TT = TensorGPU).
Aquí hay un ejemplo simple tomado de ''Diseño moderno C ++ - Programación genérica y patrones de diseño aplicados'' por Andrei Alexandrescu:
Utiliza una clase con parámetros de plantilla de plantilla para implementar el patrón de políticas:
// Library code
template <template <class> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget>
{
...
};
Él explica: Normalmente, la clase host ya conoce, o puede deducir fácilmente, el argumento de la plantilla de la clase de política. En el ejemplo anterior, WidgetManager siempre administra los objetos de tipo Widget, por lo que solicitarle al usuario que especifique Widget nuevamente en la instanciación de CreationPolicy es redundante y potencialmente peligroso. En este caso, el código de 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 manera más complicada 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 una generalizada 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;
};
Creo que necesitas usar la sintaxis de la plantilla de plantilla para pasar un parámetro cuyo tipo es una plantilla que depende de 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 que esta función tratara 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, ¡es útil 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 idear 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 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 aceptarlos. Afortunadamente, debido a la deducción de tipos, 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, solo 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 auto
c ++ 11. 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 como prefiero escribir este tipo de código.
En la solución con las plantillas variadic proporcionadas por pfalcon, me resultó difícil especializar realmente el operador ostream para std :: map debido a la naturaleza codiciosa de la especialización variadic. 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;
}
En realidad, el uso de casos para los parámetros de plantillas de plantillas es bastante obvio. Una vez que sepa que C ++ stdlib tiene un agujero enorme al no definir los operadores de salida de flujo para los tipos de contenedores 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 vector es el mismo, porque forward_list es el mismo, en realidad, incluso para una multitud de tipos de mapas sigue siendo el mismo. Esas clases de plantillas no tienen nada en común, excepto la meta-interfaz / protocolo, y el uso del parámetro de plantilla de plantilla permite capturar los elementos comunes en todas ellas. 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 tener en cuenta que aceptan 4 parámetros de plantilla, por lo que necesitaríamos otra versión del operador << anterior con un parámetro de plantilla de plantilla de 4 arg. También veríamos que std: pair intenta procesarse 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 args de plantillas de plantillas variadic), 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
Esto es lo que 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 para:
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();
}
Mejora la legibilidad de su código, proporciona seguridad de tipo adicional y ahorra algunos esfuerzos de compilación.
Si desea imprimir cada elemento de un contenedor, puede usar el siguiente código sin el 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 plantilla de parametro 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'';
}
Supongamos que pasa un número entero, diga print_container(3)
. Para el primer caso, el compilador creará una instancia de la plantilla que se quejará del uso de c
en el bucle for, este último no creará una instancia de la plantilla, ya que no se puede encontrar ningún tipo coincidente.
En términos generales, si su plantilla / función de plantilla está diseñada para manejar la clase de plantilla como parámetro de plantilla, es mejor dejarlo claro.
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 directamente los demás parámetros de plantilla a la plantilla derivada ; la "interfaz" todavía los recibe.
Esto también le permite crear typedefs en la "interfaz" que depende de los parámetros de tipo, a los que se podrá acceder desde la plantilla derivada.
El typedef anterior no funciona porque no puede typedef a una plantilla no especificada. Sin embargo, esto funciona (y C ++ 11 tiene soporte nativo para typedefs de plantillas):
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 tipo_interfaz derivado para cada creación de instancias de la plantilla derivada, a menos que haya otro truco que no haya aprendido todavía.