Quais são alguns usos dos parâmetros do modelo?


Respostas:


197

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 Hestá 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::vectorpossui 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.


1
Se f é uma função definida pelo usuário de uma biblioteca, é feio que o usuário precise passar std :: alocador <T> como argumento. Eu esperava que a versão sem o argumento std :: alocador funcionasse usando o parâmetro padrão std :: vector. Existem atualizações neste wrt C ++ 0x?
amit

Bem, você não precisa fornecer um alocador. O importante é que o parâmetro do modelo foi definido sobre o número correto de argumentos. Mas a função não deve se preocupar o que é seus "tipos" ou significado, a seguir funciona bem em C ++ 98:template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
pfalcon

Eu me pergunto por que a instanciação é f<vector,int>e não f<vector<int>>.
amigos estão dizendo sobre bobobobo

2
@obobobo Estes dois significam coisas diferentes. f<vector,int>meios f<ATemplate,AType>, f<vector<int>>meiosf<AType>
user362515

@phaedrus: (muito mais tarde ...) pontos bons, melhorou o exemplo para fazer o alocador genérico eo exemplo mais claro :-)
Evan Teran

163

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 

9
Este é um exemplo tão bom de parâmetros de modelo de modelo, pois mostra um caso com o qual todos já tiveram que lidar.
Ravenwater

3
Essa é a resposta mais interessante para mim nos modelos C ++. @WhozCraig Como você conseguiu os detalhes da expansão do modelo?
Arun

3
O @Arun gcc suporta uma macro chamada __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).
precisa saber é o seguinte

19
O parâmetro do modelo aqui não está realmente agregando valor. Você também pode usar um parâmetro de modelo regular como qualquer instância específica de um modelo de classe.
David Stone

9
Eu tenho que concordar com David Stone. Não há nenhum ponto para o parâmetro do modelo aqui. Seria muito mais simples e igualmente eficaz criar um modelo simples (modelo <typename Container>). Eu sei que este post é bastante antigo, então estou adicionando apenas meus 2 centavos para pessoas que tropeçam nessa resposta à procura de informações sobre modelos de modelo.
Jim Vargo

67

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;

1
A pergunta solicitada especificamente para outros exemplos que não o padrão de política.
user2913094

Cheguei a essa pergunta exatamente neste livro. Uma observação importante é que os parâmetros do modelo também aparecem no capítulo Typelist e no capítulo Geração de classes com Typelists .
Victor

18

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).


Você pode forçar a dedução de T com algo como: "modelo <classe T, modelo <T> TT> CLayerT" e "classe CLayerCuda: public CLayerT <TensorGPU <flutuador>>"? No caso de você não precisa de um TT <otherT>
NicoBerrogorry

NUNCA MENTE: modelo <modelo <classe T> classe U> classe B1 {}; de ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/… de uma pesquisa rápida no google
NicoBerrogorry 18/04/2018

12

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.


Eu precisava dessa solução exata para algum código (obrigado!). Embora ele funciona, eu não entendo como a classe de modelo derivedpode ser usado sem os seus argumentos de modelo, ou seja, a linhatypedef typename interface<derived, VALUE> type;
Carlton

@ Carllton funciona basicamente porque o parâmetro de modelo correspondente que está sendo preenchido é definido como a 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 typenameque significa que ele precisa ser preenchido por um tipo regular; o templatemetatype significa que ele precisa ser preenchido com uma referência a um modelo. deriveddefine um modelo que aceita um typenameparâmetro metatipado, para que ele se ajuste à conta e possa ser referenciado aqui. Faz sentido?
Mark McKenna

C ++ 11 ainda está typedef. Além disso, você pode evitar a duplicata intno seu primeiro exemplo, usando uma construção padrão como uma value_typeno tipo DERIVED.
rubenvb

Esta resposta não tem como alvo C ++ 11; Mencionei o C ++ 11 apenas para dizer que você pode solucionar o typedefproblema do bloco 2. Mas o ponto 2 é válido, eu acho ... sim, isso provavelmente seria uma maneira mais simples de fazer a mesma coisa.
Mark McKenna

7

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

4

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

2

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

2

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.


1

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).

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.