Quali sono alcuni usi dei parametri del modello di modello?


Risposte:


197

Penso che sia necessario utilizzare la sintassi del modello di modello per passare un parametro il cui tipo è un modello dipendente da un altro modello come questo:

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

Ecco Hun modello, ma volevo che questa funzione gestisse tutte le specializzazioni di H.

NOTA : sto programmando c ++ da molti anni e ne ho avuto bisogno solo una volta. Trovo che sia una funzionalità raramente necessaria (ovviamente utile quando ne hai bisogno!).

Ho cercato di pensare a buoni esempi e ad essere sincero, il più delle volte non è necessario, ma facciamo un esempio. Facciamo finta che std::vector non abbia un typedef value_type.

Quindi come scriveresti una funzione che può creare variabili del giusto tipo per gli elementi vettori? Questo funzionerebbe.

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::vectorha due parametri modello, tipo e allocatore, quindi abbiamo dovuto accettarli entrambi. Fortunatamente, a causa della deduzione del tipo, non dovremo scrivere esplicitamente il tipo esatto.

che puoi usare in questo modo:

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

o meglio ancora, possiamo semplicemente usare:

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

AGGIORNAMENTO : Anche questo esempio inventato, sebbene illustrativo, non è più un esempio sorprendente a causa dell'introduzione di c ++ 11 auto. Ora la stessa funzione può essere scritta come:

template <class Cont>
void f(Cont &v) {

    auto temp = v.back();
    v.pop_back();
    // Do some work on temp

    std::cout << temp << std::endl;
}

ed è così che preferirei scrivere questo tipo di codice.


1
Se f è una funzione definita dall'utente di una libreria, è brutto che l'utente debba passare std :: allocator <T> come argomento. Mi sarei aspettato che la versione senza l'argomento std :: allocator avesse funzionato usando il parametro predefinito di std :: vector. Ci sono aggiornamenti su questo wrt C ++ 0x?
Amit

Bene, non è necessario fornire allocatore. L'importante è che il parametro del modello di modello sia stato definito sul numero corretto di argomenti. Ma alla funzione non dovrebbe importare quali siano i loro "tipi" o significato, i seguenti funzionano bene in C ++ 98:template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
pfalcon

Mi chiedo perché l'istanza sia f<vector,int>e non f<vector<int>>.
Bobobobo,

2
@bobobobo Queste due cose significano cose diverse. f<vector,int>significa f<ATemplate,AType>, f<vector<int>>significaf<AType>
user362515

@phaedrus: (molto più tardi ...) punti positivi, migliorato l'esempio per rendere generico l'allocatore e l'esempio più chiaro :-)
Evan Teran

163

In realtà, il caso d'uso per i parametri del modello di modello è piuttosto ovvio. Una volta appreso che C ++ stdlib ha un buco nel non definire operatori di output del flusso per tipi di container standard, si procederà a scrivere qualcosa del tipo:

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

Quindi capiresti che il codice per il vettore è lo stesso, perché forward_list è lo stesso, in realtà, anche per una moltitudine di tipi di mappe è sempre lo stesso. Queste classi di template non hanno nulla in comune tranne la meta-interfaccia / protocollo e l'uso del parametro template per template consente di catturare la comunanza in tutte. Prima di continuare a scrivere un modello, vale la pena controllare un riferimento per ricordare che i contenitori di sequenza accettano 2 argomenti modello - per tipo di valore e allocatore. Mentre l'allocatore è predefinito, dovremmo comunque tener conto della sua esistenza nel nostro operatore modello <<:

template<template <typename, typename> class Container, class V, class A>
std::ostream& operator<<(std::ostream& out, Container<V, A> const& v)
...

Voila, che funzionerà automagicamente per tutti i contenitori di sequenze presenti e futuri che aderiscono al protocollo standard. Per aggiungere mappe al mix, è necessario dare una sbirciatina al riferimento per notare che accettano 4 parametri del template, quindi avremmo bisogno di un'altra versione dell'operatore << sopra con il template del template 4-arg param. Vedremmo anche che std: pair cerca di essere renderizzato con l'operatore 2-arg << per i tipi di sequenza che abbiamo definito in precedenza, quindi forniremmo una specializzazione solo per std :: pair.

A proposito, con C + 11 che consente modelli variadici (e quindi dovrebbe consentire argomenti modello template variadici), sarebbe possibile avere un singolo operatore << per dominarli tutti. Per esempio:

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

Produzione

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
Questo è un esempio così dolce di parametri del modello di modello, in quanto mostra un caso che tutti hanno dovuto affrontare.
Ravenwater

3
Questa è la risposta più sveglia per me nei modelli C ++. @WhozCraig Come hai ottenuto i dettagli di espansione del modello?
Arun,

3
@Arun gcc supporta una macro chiamata __PRETTY_FUNCTION__, che, tra le altre cose, riporta le descrizioni dei parametri del modello in testo semplice. lo fa anche clang. Una funzionalità più utile a volte (come puoi vedere).
WhozCraig,

20
Il parametro del modello di modello qui non aggiunge realmente alcun valore. Puoi anche usare un normale parametro template come una determinata istanza di un template di classe.
David Stone,

9
Sono d'accordo con David Stone. Non ha senso il parametro modello modello qui. Sarebbe molto più semplice ed ugualmente efficace creare un modello semplice (template <nome contenitore>). So che questo post è piuttosto vecchio, quindi sto solo aggiungendo i miei 2 centesimi per le persone che si imbattono in questa risposta alla ricerca di informazioni sui modelli di modello.
Jim Vargo,

67

Ecco un semplice esempio tratto da "Modern C ++ Design - Modelli di programmazione e progettazione generici applicati" di Andrei Alexandrescu:

Utilizza una classe con i parametri del modello di modello per implementare il modello di politica:

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

Spiega: in genere, la classe host conosce già o può facilmente dedurre l'argomento modello della classe politica. Nell'esempio sopra, WidgetManager gestisce sempre oggetti di tipo Widget, pertanto richiedere all'utente di specificare nuovamente Widget nell'istanza di CreationPolicy è ridondante e potenzialmente pericoloso. In questo caso, il codice della libreria può utilizzare i parametri del modello di modello per specificare le politiche.

L'effetto è che il codice client può utilizzare 'WidgetManager' in un modo più elegante:

typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;

Invece del modo più ingombrante e soggetto a errori che una definizione priva di argomenti del modello di modello avrebbe richiesto:

typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;

1
La domanda ha richiesto specificamente esempi diversi dal modello di politica.
user2913094,

Sono arrivato a questa domanda esattamente da questo libro. Una nota degna di nota è che i parametri del modello di modello compaiono anche nel capitolo Elenco dei tipi e nel capitolo Generazione di classi con elenchi dei tipi .
Victor

18

Ecco un altro esempio pratico dalla mia libreria di rete neurale convoluzionale CUDA . Ho il seguente modello di classe:

template <class T> class Tensor

che in realtà implementa la manipolazione di matrici n-dimensionali. C'è anche un modello di classe figlio:

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

che implementa la stessa funzionalità ma nella GPU. Entrambi i modelli possono funzionare con tutti i tipi di base, come float, double, int, ecc. E ho anche un modello di classe (semplificato):

template <template <class> class TT, class T> class CLayerT: public Layer<TT<T> >
{
    TT<T> weights;
    TT<T> inputs;
    TT<int> connection_matrix;
}

Il motivo qui per avere la sintassi del modello di modello è perché posso dichiarare l'implementazione della classe

class CLayerCuda: public CLayerT<TensorGPU, float>

che avrà sia pesi che input di tipo float e su GPU, ma connection_matrix sarà sempre int, sulla CPU (specificando TT = Tensor) o sulla GPU (specificando TT = TensorGPU).


Puoi forzare la deduzione di T con qualcosa del tipo: "template <classe T, template <T> TT> CLayerT" e "class CLayerCuda: public CLayerT <TensorGPU <float>>"? Nel caso in cui non avessi bisogno di un TT <otherT>
NicoBerrogorry,

MAI MENTE: template <template <class T> class U> class B1 {}; da ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/… da una rapida ricerca su Google
NicoBerrogorry,

12

Supponiamo che tu stia utilizzando CRTP per fornire una "interfaccia" per una serie di modelli figlio; e sia il genitore che il figlio sono parametrici in altri argomenti del modello:

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;

Nota la duplicazione di 'int', che in realtà è lo stesso parametro di tipo specificato in entrambi i modelli. È possibile utilizzare un modello di modello per DERIVATO per evitare questa duplicazione:

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;

Si noti che si sta eliminando fornendo direttamente gli altri parametri del modello al modello derivato ; l '"interfaccia" li riceve ancora.

Ciò consente anche di creare typedef nell '"interfaccia" che dipendono dai parametri del tipo, che saranno accessibili dal modello derivato.

Il typedef sopra riportato non funziona perché non è possibile scrivere su un modello non specificato. Funziona comunque (e C ++ 11 ha il supporto nativo per i typedef dei template):

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

typedef typename derived_interface_type<int>::type derived_t;

Sfortunatamente hai bisogno di un derivato_interfaccia_tipo per ogni istanza del modello derivato, a meno che non ci sia un altro trucco che non ho ancora imparato.


Avevo bisogno di questa soluzione esatta per un po 'di codice (grazie!). Anche se funziona, non capisco come la classe template derivedpossa essere usata senza i suoi argomenti template, ovvero la rigatypedef typename interface<derived, VALUE> type;
Carlton,

@Carlton funziona sostanzialmente perché il parametro del modello corrispondente da compilare è definito come a template <typename>. In un certo senso puoi pensare ai parametri del template come avere un 'metatype'; il normale metatype per un parametro template è typenameche significa che deve essere compilato da un tipo regolare; il templatemetatipo significa che deve essere riempito con un riferimento a un modello. deriveddefinisce un modello che accetta un typenameparametro metatyped, quindi si adatta al conto e può essere indicato qui. Ha senso?
Mark McKenna il

C ++ 11 ancora typedef. Inoltre, puoi evitare il duplicato intnel tuo primo esempio usando un costrutto standard come un value_typenel tipo DERIVED.
rubenvb,

Questa risposta in realtà non ha come target C ++ 11; Ho fatto riferimento a C ++ 11 solo per dire che puoi aggirare il typedefproblema dal blocco 2. Ma il punto 2 è valido Penso che ... sì, probabilmente sarebbe un modo più semplice di fare la stessa cosa.
Mark McKenna,

7

Questo è quello che ho incontrato:

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

};

Può essere risolto per:

template<class A>
class B
{
  A& a;
};

template< template<class> class B>
class A
{
  B<A> b;
};

class AInstance : A<B> //happy
{

};

o (codice di lavoro):

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

Nella soluzione con modelli variadici forniti da pfalcon, ho trovato difficile specializzare effettivamente l'operatore ostream per std :: map a causa della natura avida della specializzazione variadica. Ecco una leggera revisione che ha funzionato per me:

#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

Eccone uno generalizzato da qualcosa che ho appena usato. Lo sto pubblicando poiché è un esempio molto semplice e dimostra un caso d'uso pratico insieme a argomenti predefiniti:

#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

Migliora la leggibilità del codice, fornisce una sicurezza aggiuntiva del tipo e risparmia alcuni sforzi del compilatore.

Supponi di voler stampare ogni elemento di un contenitore, puoi usare il seguente codice senza il parametro template modello

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

o con parametro modello modello

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

Supponiamo di passare in un numero intero dire print_container(3). Nel primo caso, il modello verrà istanziato dal compilatore che si lamenterà dell'uso di cnel ciclo for, il secondo non creerà un'istanza del modello poiché non è possibile trovare alcun tipo di corrispondenza.

In generale, se la classe / funzione del modello è progettata per gestire la classe del modello come parametro del modello, è meglio chiarirlo.


1

Lo uso per i tipi con versione.

Se si dispone di un tipo con versione tramite un modello come MyType<version>, è possibile scrivere una funzione in cui è possibile acquisire il numero di versione:

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

Quindi puoi fare cose diverse a seconda della versione del tipo che viene passato invece di avere un sovraccarico per ogni tipo. Puoi anche avere funzioni di conversione che accettano MyType<Version>e restituiscono MyType<Version+1>, in modo generico, e persino che le fanno ricorso per avere una ToNewest()funzione che restituisce l'ultima versione di un tipo da qualsiasi versione precedente (molto utile per i registri che potrebbero essere stati archiviati qualche tempo fa ma devono essere elaborati con lo strumento più recente di oggi).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.