web-dev-qa-db-ger.com

Was sind einige Verwendungen von Vorlagenvorlagenparametern?

Ich habe einige Beispiele für C++ gesehen, die Vorlagenvorlagenparameter (dh Vorlagen, die Vorlagen als Parameter verwenden) verwenden, um richtlinienbasiertes Klassendesign durchzuführen. Welchen anderen Nutzen hat diese Technik?

202
Ferruccio

Ich denke, Sie müssen eine Vorlagenvorlagen-Syntax verwenden, um einen Parameter zu übergeben, dessen Typ eine Vorlage ist, die von einer anderen Vorlage wie dieser abhängt:

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

Hier ist H eine Vorlage, aber ich wollte, dass diese Funktion alle Spezialisierungen von H behandelt.

NOTE: Ich programmiere C++ seit vielen Jahren und habe dies nur einmal benötigt. Ich finde, dass es eine selten benötigte Funktion ist (natürlich nützlich, wenn Sie es brauchen!).

Ich habe versucht, mir gute Beispiele vorzustellen, und um ehrlich zu sein, ist dies meistens nicht notwendig, aber lassen Sie uns ein Beispiel nennen. Nehmen wir an, dass std::vectornicht einen typedef value_type hat. 

Wie würden Sie also eine Funktion schreiben, die für die Vektorenelemente Variablen des richtigen Typs 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;
}

NOTE: we std::vector hat zwei Vorlagenparameter, type und Allocator, also mussten wir beide akzeptieren. Glücklicherweise müssen wir aufgrund des Typabzugs den genauen Typ nicht explizit ausschreiben.

welche kannst du so benutzen:

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

oder besser noch, wir können einfach verwenden:

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

UPDATE: Auch wenn dieses Beispiel zwar illustrativ ist, ist es aufgrund der Einführung von auto in c ++ 11 kein erstaunliches Beispiel mehr. Nun kann die gleiche 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.

167
Evan Teran

Tatsächlich ist die Verwendung von Template-Vorlagen-Parametern ziemlich offensichtlich. Sobald Sie erfahren, dass in C++ stdlib keine Stream-Ausgabeoperatoren für Standardcontainertypen definiert werden, werden Sie folgende Schritte ausführen:

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 vector genau derselbe ist, denn forward_list ist derselbe. Selbst für eine Vielzahl von Kartentypen ist er immer noch derselbe. Diese Vorlagenklassen haben mit Ausnahme der Meta-Schnittstelle/des Protokolls nichts gemeinsam. Durch die Verwendung von Vorlagenvorlagenparametern kann die Gemeinsamkeit in allen Klassen erfasst werden. Bevor Sie jedoch mit dem Erstellen einer Vorlage fortfahren, sollten Sie eine Referenz überprüfen, um sich daran zu erinnern, dass Sequenzcontainer zwei Vorlagenargumente akzeptieren - für den Werttyp und den Zuweiser. Während der Zuweiser in Verzug ist, sollten wir dennoch dessen Vorhandensein 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 Karten zu der Mischung hinzuzufügen, ist ein Blick auf die Referenz erforderlich, um zu beachten, dass sie 4 Vorlagenparameter akzeptieren. Daher benötigen wir eine andere Version des Operators << mit 4-Vorlagenvorlage param. Wir würden auch sehen, dass std: pair versucht, mit dem 2-arg-Operator << für die zuvor definierten Sequenztypen gerendert zu werden. Daher würden wir eine Spezialisierung nur für std :: pair anbieten. 

Übrigens, mit C + 11, das variadische Templates zulässt (und daher variable Templates für Template-Vorlagen zulassen soll), wäre es möglich, einen einzelnen Operator << zu haben, um sie alle zu beherrschen. Zum Beispiel:

#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 
139
pfalcon

Hier ist ein einfaches Beispiel aus 'Modernes C++ - Design - Generische Programmier- und Designmuster angewendet' von Andrei Alexandrescu:

Er verwendet Klassen 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 Host-Klasse das Vorlagenargument der Richtlinienklasse bereits oder kann es leicht ableiten. Im obigen Beispiel verwaltet WidgetManager immer Objekte des Typs Widget, sodass der Benutzer bei der Instantiierung von CreationPolicy erneut angeben muss, dass er redundant und potenziell gefährlich ist. In diesem Fall kann der Bibliothekscode Vorlagenvorlagenparameter zum Angeben von Richtlinien verwenden.

Der Effekt ist, dass der Clientcode 'WidgetManager' auf eine elegantere Weise verwenden kann:

typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;

Anstelle der umständlicheren und fehleranfälligeren Art und Weise, die eine Definition ohne Template-Vorlagenargumente erfordert hätte:

typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;
60
yoav.aviram

Hier ist ein weiteres praktisches Beispiel aus meiner CUDA Convolutional Neural Network Library . Ich habe die folgende Klassenvorlage:

template <class T> class Tensor

das ist eigentlich eine n-dimensionale Matrizenmanipulation. Es gibt auch eine untergeordnete Klassenvorlage:

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

welche die gleiche Funktionalität aber in GPU implementiert. Beide Schablonen können mit allen Grundtypen arbeiten, wie float, double, int usw .. Und ich habe auch 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, Template-Syntax zu haben, liegt darin, dass ich die Implementierung der Klasse deklarieren kann

class CLayerCuda: public CLayerT<TensorGPU, float>

die sowohl Gewichte als auch Eingaben vom Typ float und auf GPU haben wird, connection_matrix ist jedoch immer int, entweder auf der CPU (durch Angabe von TT = Tensor) oder auf der GPU (durch Angabe von TT = TensorGPU).

18

Angenommen, Sie verwenden CRTP, um eine "Schnittstelle" für einen Satz untergeordneter Vorlagen bereitzustellen. und das Elternteil und 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 Duplikation von 'int', bei dem es sich tatsächlich um denselben Typparameter handelt, der in beiden Vorlagen angegeben ist. 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 es unterlassen, die anderen Vorlagenparameter direkt in die Vorlage derivative einzufügen. Die "Schnittstelle" empfängt sie immer noch.

Auf diese Weise können Sie auch Typedefs in der "Schnittstelle" aufbauen, 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 tippen können. Dies funktioniert jedoch (und C++ 11 unterstützt native Template-Typedefs):

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

typedef typename derived_interface_type<int>::type derived_t;

Leider benötigen Sie für jede Instanziierung der abgeleiteten Vorlage einen abgeleiteten_Interface-Typ, es sei denn, es gibt noch einen weiteren Trick, den ich noch nicht gelernt habe.

9
Mark McKenna

Darauf bin ich gestoßen:

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 mit:

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
Cookie

In der von pfalcon zur Verfügung gestellten Lösung mit variadischen Vorlagen fand ich es schwierig, den ostream-Operator tatsächlich für std :: map zu spezialisieren, da die variadische Spezialisierung gierig ist. 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;
}
4

Hier ist eine Verallgemeinerung von etwas, das ich gerade verwendet habe. Ich poste es, da es ein very einfaches Beispiel ist, und es zeigt einen praktischen Anwendungsfall zusammen mit Standardargumenten:

#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
imallett

Es verbessert die Lesbarkeit Ihres Codes, bietet zusätzliche Sicherheit 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 template template parameter

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, sagen Sie print_container(3). Im ersten Fall wird die Vorlage vom Compiler instanziiert, der sich über die Verwendung von c in der for-Schleife beschwert. Die letztere löscht die Vorlage überhaupt nicht, da kein übereinstimmender Typ gefunden werden kann. 

Wenn Ihre Schablonenklasse/-funktionalität so ausgelegt ist, dass sie Schablonenklasse als Vorlagenparameter behandelt, ist es im Allgemeinen besser, dies klar zu machen. 

0
colin