Was sind einige Verwendungszwecke von Vorlagenvorlagenparametern?


238

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?


4
Ich kam aus der anderen Richtung (FP, Haskell usw.) und landete auf dieser: stackoverflow.com/questions/2565097/higher-kinded-types-with-c
Erik Kaplun

Antworten:


197

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


1
Wenn f eine vom Benutzer einer Bibliothek definierte Funktion ist, ist es hässlich, dass der Benutzer std :: allocator <T> als Argument übergeben muss. Ich hätte erwartet, dass die Version ohne das Argument std :: allocator mit dem Standardparameter std :: vector funktioniert hat. Gibt es Updates für dieses C ++ 0x?
Amit

Nun, Sie müssen keinen Allokator bereitstellen. Wichtig ist, dass der Vorlagenvorlagenparameter über die richtige Anzahl von Argumenten definiert wurde. Aber die Funktion sollte sich nicht darum kümmern, was ihre "Typen" oder Bedeutungen sind, folgendes funktioniert gut in C ++ 98:template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
pfalcon

Ich frage mich, warum Instanziierung ist f<vector,int>und nicht f<vector<int>>.
Bobobobo

2
@ Bobobobo Diese beiden bedeuten verschiedene Dinge. f<vector,int>bedeutet f<ATemplate,AType>, f<vector<int>>bedeutetf<AType>
user362515

@phaedrus: (viel später ...) gute Punkte, verbesserte das Beispiel, um den Allokator generisch und das Beispiel klarer zu machen :-)
Evan Teran

163

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 

9
Dies ist ein so süßes Beispiel für Vorlagenvorlagenparameter, da es einen Fall zeigt, mit dem sich jeder befassen musste.
Ravenwater

3
Dies ist für mich die erwachendste Antwort in C ++ - Vorlagen. @WhozCraig Wie haben Sie die Details zur Vorlagenerweiterung erhalten?
Arun

3
@Arun gcc unterstützt ein Makro namens __PRETTY_FUNCTION__, das unter anderem Vorlagenparameterbeschreibungen im Klartext meldet. Clang macht es auch. Manchmal eine sehr praktische Funktion (wie Sie sehen können).
WhozCraig

19
Der Vorlagenvorlagenparameter hier fügt keinen wirklichen Wert hinzu. Sie können auch einfach einen regulären Vorlagenparameter als eine bestimmte Instanz einer Klassenvorlage verwenden.
David Stone

9
Ich muss David Stone zustimmen. Der Parameter der Vorlagenvorlage hat hier keinen Sinn. Es wäre viel einfacher und ebenso effektiv, eine einfache Vorlage zu erstellen (Vorlage <Typname Container>). Ich weiß, dass dieser Beitrag ziemlich alt ist, daher addiere ich meine 2 Cent nur für Leute, die über diese Antwort stolpern und nach Informationen über Vorlagenvorlagen suchen.
Jim Vargo

67

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;

1
Die Frage wurde speziell für andere Beispiele als das Richtlinienmuster angefordert.
user2913094

Ich bin genau aus diesem Buch auf diese Frage gekommen. Beachten Sie, dass die Vorlagenvorlagenparameter auch im Kapitel Typelist und im Kapitel Klassengenerierung mit Typelists angezeigt werden .
Victor

18

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


Können Sie den Abzug von T erzwingen mit: "template <Klasse T, template <T> TT> CLayerT" und "class CLayerCuda: public CLayerT <TensorGPU <float>"? Für den Fall, dass Sie keinen TT <otherT>
brauchten

NIEMALS VERSTANDEN: Vorlage <Vorlage <Klasse T> Klasse U> Klasse B1 {}; von ibm.com/support/knowledgecenter/de/SSLTBW_2.3.0/… von einer schnellen Google-Suche
NicoBerrogorry

12

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.


Ich brauchte genau diese Lösung für Code (danke!). Obwohl es funktioniert, verstehe ich nicht, wie die Vorlagenklasse derivedohne ihre Vorlagenargumente verwendet werden kann, dh die Zeiletypedef typename interface<derived, VALUE> type;
Carlton

@Carlton funktioniert grundsätzlich, weil der entsprechende Vorlagenparameter, der gefüllt wird, als definiert ist template <typename>. In gewissem Sinne können Sie sich die Vorlagenparameter als einen "Metatyp" vorstellen. Der normale Metatyp für einen Vorlagenparameter ist, typenamewas bedeutet, dass er von einem regulären Typ gefüllt werden muss. Der templateMetatyp bedeutet, dass er mit einem Verweis auf eine Vorlage gefüllt werden muss. Definiert derivedeine Vorlage, die einen typenamemetatypisierten Parameter akzeptiert , sodass sie zur Rechnung passt und hier referenziert werden kann. Sinn ergeben?
Mark McKenna

C ++ 11 noch nicht typedef. Sie können das Duplikat intin Ihrem ersten Beispiel auch vermeiden, indem Sie ein Standardkonstrukt wie a value_typevom Typ DERIVED verwenden.
Rubenvb

Diese Antwort zielt nicht auf C ++ 11 ab. Ich habe auf C ++ 11 verwiesen, nur um zu sagen, dass Sie das typedefProblem 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.
Mark McKenna

7

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

4

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

2

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

2

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


1

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.