Ho visto alcuni esempi di C ++ che utilizzano i parametri del modello di modello (ovvero modelli che accettano modelli come parametri) per progettare classi basate su criteri. Quali altri usi ha questa tecnica?
Ho visto alcuni esempi di C ++ che utilizzano i parametri del modello di modello (ovvero modelli che accettano modelli come parametri) per progettare classi basate su criteri. Quali altri usi ha questa tecnica?
Risposte:
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 H
un 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::vector
ha 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.
template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
f<vector,int>
e non f<vector<int>>
.
f<vector,int>
significa f<ATemplate,AType>
, f<vector<int>>
significaf<AType>
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
__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).
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;
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).
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.
derived
possa essere usata senza i suoi argomenti template, ovvero la rigatypedef typename interface<derived, VALUE> type;
template <typename>
. In un certo senso puoi pensare ai parametri del template come avere un 'metatype'; il normale metatype per un parametro template è typename
che significa che deve essere compilato da un tipo regolare; il template
metatipo significa che deve essere riempito con un riferimento a un modello. derived
definisce un modello che accetta un typename
parametro metatyped, quindi si adatta al conto e può essere indicato qui. Ha senso?
typedef
. Inoltre, puoi evitare il duplicato int
nel tuo primo esempio usando un costrutto standard come un value_type
nel tipo DERIVED.
typedef
problema dal blocco 2. Ma il punto 2 è valido Penso che ... sì, probabilmente sarebbe un modo più semplice di fare la stessa cosa.
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();
}
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;
}
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;
};
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 c
nel 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.
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).