Ich habe einige Beispiele für C ++ gesehen, in denen Vorlagenvorlagenparameter (dh Vorlagen, die Vorlagen als Parameter verwenden) für das richtlinienbasierte Klassendesign verwendet werden. Welche anderen Verwendungszwecke hat diese Technik?
Ich habe einige Beispiele für C ++ gesehen, in denen Vorlagenvorlagenparameter (dh Vorlagen, die Vorlagen als Parameter verwenden) für das richtlinienbasierte Klassendesign verwendet werden. Welche anderen Verwendungszwecke hat diese Technik?
Antworten:
Ich denke, Sie müssen die Vorlagenvorlagensyntax verwenden, um einen Parameter zu übergeben, dessen Typ eine Vorlage ist, die von einer anderen Vorlage wie dieser abhängig ist:
template <template<class> class H, class S>
void f(const H<S> &value) {
}
Hier H
ist eine Vorlage, aber ich wollte, dass diese Funktion alle Spezialisierungen von behandelt H
.
HINWEIS : Ich programmiere seit vielen Jahren C ++ und habe dies nur einmal benötigt. Ich finde, dass es eine selten benötigte Funktion ist (natürlich praktisch, wenn Sie es brauchen!).
Ich habe versucht, mir gute Beispiele auszudenken, und um ehrlich zu sein, ist dies die meiste Zeit nicht notwendig, aber lassen Sie uns ein Beispiel erfinden. Stellen wir uns vor, std::vector
das hätte keine typedef value_type
.
Wie würden Sie also eine Funktion schreiben, die Variablen des richtigen Typs für die Vektorelemente erstellen kann? Das würde funktionieren.
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;
}
HINWEIS : Hatstd::vector
zwei Vorlagenparameter, Typ und Allokator, daher mussten wir beide akzeptieren. Glücklicherweise müssen wir aufgrund des Typabzugs den genauen Typ nicht explizit ausschreiben.
die Sie so verwenden können:
f<std::vector, int>(v); // v is of type std::vector<int> using any allocator
oder noch besser, wir können einfach verwenden:
f(v); // everything is deduced, f can deal with a vector of any type!
UPDATE : Auch dieses erfundene Beispiel ist zwar illustrativ, aber aufgrund der Einführung von C ++ 11 kein erstaunliches Beispiel mehr auto
. Jetzt kann dieselbe Funktion geschrieben werden als:
template <class Cont>
void f(Cont &v) {
auto temp = v.back();
v.pop_back();
// Do some work on temp
std::cout << temp << std::endl;
}
So würde ich es vorziehen, diese Art von Code zu schreiben.
template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
f<vector,int>
und nicht f<vector<int>>
.
f<vector,int>
bedeutet f<ATemplate,AType>
, f<vector<int>>
bedeutetf<AType>
Tatsächlich ist die Verwendung von Vorlagenvorlagenparametern ziemlich offensichtlich. Sobald Sie erfahren, dass C ++ stdlib eine Lücke aufweist, in der keine Stream-Ausgabeoperatoren für Standardcontainertypen definiert werden, schreiben Sie Folgendes:
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;
}
Dann würden Sie herausfinden, dass der Code für den Vektor genau der gleiche ist, denn forward_list ist der gleiche, selbst für eine Vielzahl von Kartentypen ist er immer noch der gleiche. Diese Vorlagenklassen haben außer der Meta-Schnittstelle / dem Meta-Protokoll nichts gemeinsam. Durch die Verwendung des Vorlagenvorlagenparameters kann die Gemeinsamkeit in allen Klassen erfasst werden. Bevor Sie jedoch mit dem Schreiben einer Vorlage fortfahren, sollten Sie einen Verweis überprüfen, um sich daran zu erinnern, dass Sequenzcontainer zwei Vorlagenargumente akzeptieren - für Werttyp und Allokator. Während der Allokator standardmäßig aktiviert ist, sollten wir seine Existenz dennoch in unserem Vorlagenoperator << berücksichtigen:
template<template <typename, typename> class Container, class V, class A>
std::ostream& operator<<(std::ostream& out, Container<V, A> const& v)
...
Voila, das funktioniert automatisch für alle gegenwärtigen und zukünftigen Sequenzcontainer, die dem Standardprotokoll entsprechen. Um dem Mix Karten hinzuzufügen, muss ein Blick auf die Referenz geworfen werden, um festzustellen, dass sie 4 Vorlagenparameter akzeptieren. Daher benötigen wir eine andere Version des Operators << oben mit 4-arg-Vorlagenvorlagenparametern. Wir würden auch sehen, dass std: pair versucht, mit dem 2-arg-Operator << für zuvor definierte Sequenztypen gerendert zu werden, sodass wir eine Spezialisierung nur für std :: pair bereitstellen würden.
Übrigens, mit C + 11, das verschiedene Vorlagen zulässt (und daher Argumente für Vorlagenvorlagen zulassen sollte), wäre es möglich, einen einzigen Operator << zu haben, um sie alle zu regieren. Beispielsweise:
#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;
}
Ausgabe
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__
, das unter anderem Vorlagenparameterbeschreibungen im Klartext meldet. Clang macht es auch. Manchmal eine sehr praktische Funktion (wie Sie sehen können).
Hier ist ein einfaches Beispiel aus 'Modern C ++ Design - Generische Programmierung und angewandte Entwurfsmuster' von Andrei Alexandrescu:
Er verwendet eine Klasse mit Vorlagenvorlagenparametern, um das Richtlinienmuster zu implementieren:
// Library code
template <template <class> class CreationPolicy>
class WidgetManager : public CreationPolicy<Widget>
{
...
};
Er erklärt: Normalerweise kennt die Hostklasse das Vorlagenargument der Richtlinienklasse bereits oder kann es leicht ableiten. Im obigen Beispiel verwaltet WidgetManager immer Objekte vom Typ Widget. Daher ist es redundant und potenziell gefährlich, wenn der Benutzer Widget bei der Instanziierung von CreationPolicy erneut angeben muss. In diesem Fall kann der Bibliothekscode Vorlagenvorlagenparameter zum Festlegen von Richtlinien verwenden.
Der Effekt ist, dass der Client-Code 'WidgetManager' auf elegantere Weise verwenden kann:
typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;
Anstelle der umständlicheren und fehleranfälligeren Methode, für die eine Definition ohne Vorlagenvorlagenargumente erforderlich gewesen wäre:
typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;
Hier ist ein weiteres praktisches Beispiel aus meiner CUDA Convolutional Neural Network Library . Ich habe die folgende Klassenvorlage:
template <class T> class Tensor
Dies implementiert tatsächlich die Manipulation von n-dimensionalen Matrizen. Es gibt auch eine untergeordnete Klassenvorlage:
template <class T> class TensorGPU : public Tensor<T>
Dies implementiert die gleiche Funktionalität, jedoch in der GPU. Beide Vorlagen können mit allen Grundtypen wie float, double, int usw. verwendet werden. Außerdem habe ich eine Klassenvorlage (vereinfacht):
template <template <class> class TT, class T> class CLayerT: public Layer<TT<T> >
{
TT<T> weights;
TT<T> inputs;
TT<int> connection_matrix;
}
Der Grund für die Syntax der Vorlagenvorlage liegt darin, dass ich die Implementierung der Klasse deklarieren kann
class CLayerCuda: public CLayerT<TensorGPU, float>
Dies hat sowohl Gewichte als auch Eingaben vom Typ float und auf der GPU, aber connection_matrix ist immer int, entweder auf der CPU (durch Angabe von TT = Tensor) oder auf der GPU (durch Angabe von TT = TensorGPU).
Angenommen, Sie verwenden CRTP, um eine "Schnittstelle" für eine Reihe von untergeordneten Vorlagen bereitzustellen. und sowohl das Elternteil als auch das Kind sind in anderen Vorlagenargumenten parametrisch:
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;
Beachten Sie die Duplizierung von 'int', bei der es sich tatsächlich um denselben Typparameter handelt, der für beide Vorlagen angegeben wurde. Sie können eine Vorlagenvorlage für DERIVED verwenden, um diese Duplizierung zu vermeiden:
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;
Beachten Sie, dass Sie die direkte Bereitstellung der anderen Vorlagenparameter für die abgeleitete Vorlage eliminieren . Die "Schnittstelle" empfängt sie weiterhin.
Auf diese Weise können Sie auch Typedefs in der "Schnittstelle" erstellen, die von den Typparametern abhängen, auf die über die abgeleitete Vorlage zugegriffen werden kann.
Das obige typedef funktioniert nicht, da Sie nicht in eine nicht angegebene Vorlage typedef können. Dies funktioniert jedoch (und C ++ 11 unterstützt native Vorlagen-Typedefs):
template <typename VALUE>
struct derived_interface_type {
typedef typename interface<derived, VALUE> type;
};
typedef typename derived_interface_type<int>::type derived_t;
Sie benötigen leider einen derivative_interface_type für jede Instanziierung der abgeleiteten Vorlage, es sei denn, es gibt einen anderen Trick, den ich noch nicht gelernt habe.
derived
ohne ihre Vorlagenargumente verwendet werden kann, dh die Zeiletypedef typename interface<derived, VALUE> type;
template <typename>
. In gewissem Sinne können Sie sich die Vorlagenparameter als einen "Metatyp" vorstellen. Der normale Metatyp für einen Vorlagenparameter ist, typename
was bedeutet, dass er von einem regulären Typ gefüllt werden muss. Der template
Metatyp bedeutet, dass er mit einem Verweis auf eine Vorlage gefüllt werden muss. Definiert derived
eine Vorlage, die einen typename
metatypisierten Parameter akzeptiert , sodass sie zur Rechnung passt und hier referenziert werden kann. Sinn ergeben?
typedef
. Sie können das Duplikat int
in Ihrem ersten Beispiel auch vermeiden, indem Sie ein Standardkonstrukt wie a value_type
vom Typ DERIVED verwenden.
typedef
Problem ab Block 2 umgehen können. Aber Punkt 2 ist gültig, denke ich ... ja, das wäre wahrscheinlich eine einfachere Möglichkeit, dasselbe zu tun.
Dies ist, was ich getroffen habe:
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)>>>>>>>>
{
};
Kann gelöst werden um:
template<class A>
class B
{
A& a;
};
template< template<class> class B>
class A
{
B<A> b;
};
class AInstance : A<B> //happy
{
};
oder (Arbeitscode):
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();
}
Bei der von pfalcon bereitgestellten Lösung mit variadischen Vorlagen fiel es mir aufgrund der gierigen Natur der variadischen Spezialisierung schwer, den ostream-Operator für std :: map tatsächlich zu spezialisieren. Hier ist eine kleine Überarbeitung, die für mich funktioniert hat:
#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;
}
Hier ist eine Verallgemeinerung von etwas, das ich gerade benutzt habe. Ich poste es, da es ein sehr einfaches Beispiel ist und einen praktischen Anwendungsfall zusammen mit Standardargumenten zeigt:
#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;
};
Es verbessert die Lesbarkeit Ihres Codes, bietet zusätzliche Typensicherheit und spart einige Compiler-Anstrengungen.
Angenommen, Sie möchten jedes Element eines Containers drucken, können Sie den folgenden Code ohne Vorlagenvorlagenparameter verwenden
template <typename T> void print_container(const T& c)
{
for (const auto& v : c)
{
std::cout << v << ' ';
}
std::cout << '\n';
}
oder mit Vorlagenvorlagenparameter
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';
}
Angenommen, Sie übergeben eine Ganzzahl print_container(3)
. Im ersten Fall wird die Vorlage vom Compiler instanziiert, der sich über die Verwendung c
in der for-Schleife beschwert. Im zweiten Fall wird die Vorlage überhaupt nicht instanziiert, da kein passender Typ gefunden werden kann.
Wenn Ihre Vorlagenklasse / -funktion so ausgelegt ist, dass sie die Vorlagenklasse als Vorlagenparameter behandelt, ist es im Allgemeinen besser, dies klar zu machen.
Ich benutze es für versionierte Typen.
Wenn Sie einen Typ haben, der über eine Vorlage wie z. B. versioniert wurde MyType<version>
, können Sie eine Funktion schreiben, in der Sie die Versionsnummer erfassen können:
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)
{
...
}
}
Sie können also abhängig von der Version des übergebenen Typs verschiedene Dinge tun, anstatt für jeden Typ eine Überladung zu haben. Sie können auch Konvertierungsfunktionen haben , die in nehmen MyType<Version>
und zurück MyType<Version+1>
, in allgemeiner Weise und Rekursion sie sogar haben ToNewest()
Funktion , die die neueste Version einer Art von jeder älteren Version (sehr nützlich für die Protokolle zurückgibt , die eine Weile zurück gespeichert haben könnte müssen aber mit dem neuesten Tool von heute verarbeitet werden).