Eu já vi alguns exemplos de C ++ usando parâmetros de modelo (ou seja, modelos que usam modelos como parâmetros) para criar um design de classe baseado em políticas. Que outros usos essa técnica tem?
Eu já vi alguns exemplos de C ++ usando parâmetros de modelo (ou seja, modelos que usam modelos como parâmetros) para criar um design de classe baseado em políticas. Que outros usos essa técnica tem?
Respostas:
Eu acho que você precisa usar a sintaxe do modelo para passar um parâmetro cujo tipo é um modelo dependente de outro modelo como este:
template <template<class> class H, class S>
void f(const H<S> &value) {
}
Aqui H
está um modelo, mas eu queria que essa função lidasse com todas as especializações de H
.
NOTA : Estou programando c ++ há muitos anos e só precisei disso uma vez. Acho que é um recurso raramente necessário (é claro útil quando você precisar!).
Eu tenho tentado pensar em bons exemplos e, para ser sincero, na maioria das vezes isso não é necessário, mas vamos inventar um exemplo. Vamos fingir que std::vector
não tem um typedef value_type
.
Então, como você escreveria uma função que pode criar variáveis do tipo certo para os elementos de vetores? Isso 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
possui dois parâmetros de modelo, tipo e alocador, portanto, tivemos que aceitar os dois. Felizmente, devido à dedução de tipo, não precisamos escrever explicitamente o tipo exato.
que você pode usar assim:
f<std::vector, int>(v); // v is of type std::vector<int> using any allocator
ou melhor ainda, podemos apenas usar:
f(v); // everything is deduced, f can deal with a vector of any type!
ATUALIZAÇÃO : Mesmo este exemplo artificial, embora ilustrativo, não é mais um exemplo surpreendente devido à introdução do c ++ 11 auto
. Agora a mesma função pode ser escrita 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;
}
é assim que eu preferiria escrever esse tipo de código.
template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
f<vector,int>
e não f<vector<int>>
.
f<vector,int>
meios f<ATemplate,AType>
, f<vector<int>>
meiosf<AType>
Na verdade, o caso de uso dos parâmetros do modelo é bastante óbvio. Depois que você descobrir que o C ++ stdlib possui um buraco de não definir operadores de saída de fluxo para tipos de contêineres padrão, você deve escrever 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;
}
Então você descobriria que o código para vetor é o mesmo, pois forward_list é o mesmo, na verdade, mesmo para vários tipos de mapa, ainda é o mesmo. Essas classes de modelo não têm nada em comum, exceto a meta-interface / protocolo, e o uso do parâmetro template template permite capturar a semelhança em todas elas. Antes de continuar a escrever um modelo, vale a pena verificar uma referência para lembrar que os contêineres de sequência aceitam 2 argumentos de modelo - para o tipo de valor e o alocador. Enquanto o alocador estiver em falta, ainda devemos considerar sua existência em nosso operador de modelo <<:
template<template <typename, typename> class Container, class V, class A>
std::ostream& operator<<(std::ostream& out, Container<V, A> const& v)
...
Voila, que funcionará automaticamente para todos os contêineres de seqüência presentes e futuros que aderem ao protocolo padrão. Para adicionar mapas à mistura, seria necessário dar uma olhada na referência para observar que eles aceitam 4 parâmetros de modelo, portanto, precisaríamos de outra versão do operador << acima com o parâmetro de modelo de modelo 4-arg. Também veríamos que std: pair tenta ser renderizado com o operador 2-arg << para os tipos de sequência definidos anteriormente, para fornecer uma especialização apenas para std :: pair.
Btw, com C + 11, que permite modelos variados (e, portanto, deve permitir args de modelos variados), seria possível ter um único operador << para governar todos eles. Por exemplo:
#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;
}
Resultado
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 outras coisas, relata descrições de parâmetros de modelo em texto simples. clang faz isso também. Um recurso mais útil às vezes (como você pode ver).
Aqui está um exemplo simples, retirado de 'Design C ++ moderno - Programação genérica e padrões de design aplicados' por Andrei Alexandrescu:
Ele usa classes com parâmetros de modelo de modelo para implementar o padrão de política:
// Library code
template <template <class> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget>
{
...
};
Ele explica: Normalmente, a classe host já conhece ou pode deduzir facilmente o argumento do modelo da classe de política. No exemplo acima, o WidgetManager sempre gerencia objetos do tipo Widget, portanto, exigir que o usuário especifique o Widget novamente na instanciação da CreationPolicy é redundante e potencialmente perigoso.Neste caso, o código da biblioteca pode usar parâmetros de modelo para especificar políticas.
O efeito é que o código do cliente pode usar 'WidgetManager' de uma maneira mais elegante:
typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;
Em vez da maneira mais complicada e propensa a erros que uma definição sem argumentos de modelo de modelo teria exigido:
typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;
Aqui está outro exemplo prático da minha biblioteca de redes neurais convolucionais CUDA . Eu tenho o seguinte modelo de classe:
template <class T> class Tensor
que na verdade implementa a manipulação de matrizes n-dimensionais. Há também um modelo de classe filho:
template <class T> class TensorGPU : public Tensor<T>
que implementa a mesma funcionalidade, mas na GPU. Ambos os modelos podem funcionar com todos os tipos básicos, como float, double, int, etc. E também tenho um modelo de classe (simplificado):
template <template <class> class TT, class T> class CLayerT: public Layer<TT<T> >
{
TT<T> weights;
TT<T> inputs;
TT<int> connection_matrix;
}
O motivo aqui para ter a sintaxe do modelo é porque posso declarar a implementação da classe
class CLayerCuda: public CLayerT<TensorGPU, float>
que terá pesos e entradas do tipo float e na GPU, mas o connection_matrix sempre será int, seja na CPU (especificando TT = Tensor) ou na GPU (especificando TT = TensorGPU).
Digamos que você esteja usando o CRTP para fornecer uma "interface" para um conjunto de modelos filhos; e o pai e o filho são paramétricos em outros argumentos do modelo:
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;
Observe a duplicação de 'int', que é realmente o mesmo parâmetro de tipo especificado para os dois modelos. Você pode usar um modelo de modelo para DERIVED para evitar esta duplicação:
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;
Observe que você está eliminando o fornecimento direto de outros parâmetros de modelo para o modelo derivado ; a "interface" ainda os recebe.
Isso também permite criar typedefs na "interface" que depende dos parâmetros de tipo, que serão acessíveis a partir do modelo derivado.
O typedef acima não funciona porque você não pode digitar um modelo não especificado. Isso funciona, no entanto (e o C ++ 11 tem suporte nativo para typedefs de modelo):
template <typename VALUE>
struct derived_interface_type {
typedef typename interface<derived, VALUE> type;
};
typedef typename derived_interface_type<int>::type derived_t;
Infelizmente, você precisa de um derivado_interface_tipo para cada instanciação do modelo derivado, a menos que haja outro truque que ainda não aprendi.
derived
pode ser usado sem os seus argumentos de modelo, ou seja, a linhatypedef typename interface<derived, VALUE> type;
template <typename>
. Em certo sentido, você pode pensar nos parâmetros do modelo como tendo um 'metatipo'; o metatipo normal para um parâmetro de modelo é o typename
que significa que ele precisa ser preenchido por um tipo regular; o template
metatype significa que ele precisa ser preenchido com uma referência a um modelo. derived
define um modelo que aceita um typename
parâmetro metatipado, para que ele se ajuste à conta e possa ser referenciado aqui. Faz sentido?
typedef
. Além disso, você pode evitar a duplicata int
no seu primeiro exemplo, usando uma construção padrão como uma value_type
no tipo DERIVED.
typedef
problema do bloco 2. Mas o ponto 2 é válido, eu acho ... sim, isso provavelmente seria uma maneira mais simples de fazer a mesma coisa.
Isto é o que eu encontrei:
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)>>>>>>>>
{
};
Pode ser resolvido para:
template<class A>
class B
{
A& a;
};
template< template<class> class B>
class A
{
B<A> b;
};
class AInstance : A<B> //happy
{
};
ou (código de trabalho):
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();
}
Na solução com modelos variados fornecidos pelo pfalcon, achei difícil realmente especializar o operador ostream para std :: map devido à natureza gananciosa da especialização variável. Aqui está uma pequena revisão que funcionou para mim:
#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;
}
Aqui está um generalizado de algo que acabei de usar. Estou publicando, já que é um exemplo muito simples e demonstra um caso de uso prático, juntamente com argumentos padrão:
#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;
};
Ele melhora a legibilidade do seu código, fornece segurança extra de tipo e economiza alguns esforços do compilador.
Digamos que você queira imprimir cada elemento de um contêiner, você pode usar o seguinte código sem o parâmetro do modelo
template <typename T> void print_container(const T& c)
{
for (const auto& v : c)
{
std::cout << v << ' ';
}
std::cout << '\n';
}
ou com o parâmetro de modelo de modelo
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';
}
Suponha que você passe um número inteiro, digamos print_container(3)
. No primeiro caso, o modelo será instanciado pelo compilador, que reclamará do uso dec
no loop for, o último não instanciará o modelo, pois nenhum tipo correspondente pode ser encontrado.
De um modo geral, se sua classe / função de modelo foi projetada para manipular a classe de modelo como parâmetro de modelo, é melhor deixar claro.
Eu o uso para tipos com versão.
Se você possui um tipo com versão através de um modelo como MyType<version>
, pode escrever uma função na qual pode capturar o número da versão:
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)
{
...
}
}
Portanto, você pode fazer coisas diferentes, dependendo da versão do tipo que está sendo transmitida, em vez de ter uma sobrecarga para cada tipo. Você também pode ter funções de conversão que recebem MyType<Version>
e retornam MyType<Version+1>
, de uma maneira genérica, e até recomendam que elas tenham uma ToNewest()
função que retorna a versão mais recente de um tipo de qualquer versão anterior (muito útil para logs que podem ter sido armazenados há algum tempo) mas precisam ser processados com a ferramenta mais recente de hoje).