Jakie są zastosowania parametrów szablonu szablonu?


Odpowiedzi:


197

Myślę, że musisz użyć składni szablonu szablonu, aby przekazać parametr, którego typ jest szablonem zależnym od innego szablonu, takiego jak ten:

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

Tutaj Hjest szablon, ale chciałem, aby ta funkcja obsługiwała wszystkie specjalizacje H.

UWAGA : Programuję c ++ od wielu lat i potrzebowałem tego tylko raz. Uważam, że jest to rzadko potrzebna funkcja (oczywiście przydatna, gdy jej potrzebujesz!).

Próbowałem wymyślić dobre przykłady i szczerze mówiąc, przez większość czasu nie jest to konieczne, ale wymyślmy przykład. Udawajmy, że std::vector nie ma typedef value_type.

Jak więc napisać funkcję, która może tworzyć zmienne odpowiedniego typu dla elementów wektorów? To by działało.

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

UWAGA : std::vectorma dwa parametry szablonu, typ i alokator, więc musieliśmy zaakceptować oba z nich. Na szczęście z powodu dedukcji typu nie będziemy musieli jawnie wypisywać dokładnego typu.

którego możesz użyć w następujący sposób:

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

lub jeszcze lepiej, możemy po prostu użyć:

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

AKTUALIZACJA : Nawet ten wymyślony przykład, choć ilustracyjny, nie jest już niesamowitym przykładem ze względu na wprowadzenie c ++ 11 auto. Teraz tę samą funkcję można zapisać jako:

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

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

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

tak wolałbym pisać tego typu kod.


1
Jeśli f jest funkcją zdefiniowaną przez użytkownika biblioteki, to brzydkie jest, że użytkownik musi przekazać argument std :: alokator <T>. Spodziewałbym się, że wersja bez argumentu std :: alokator działała przy użyciu domyślnego parametru std :: vector. Czy są jakieś aktualizacje tego wrt C ++ 0x?
dniu

Nie musisz podawać alokatora. Co ważne, parametr szablonu szablonu został zdefiniowany na podstawie prawidłowej liczby argumentów. Ale funkcja nie powinien dbać, co swoje „typy” i oznacza, następujące prace dobrze w C ++ 98:template<template<class, class> class C, class T, class U> void f(C<T, U> &v)
pfalcon

Zastanawiam się, dlaczego tworzenie instancji jest f<vector,int>i nie f<vector<int>>.
bobobobo

2
@bobobobo Te dwie oznaczają różne rzeczy. f<vector,int>oznacza f<ATemplate,AType>, f<vector<int>>znaczyf<AType>
362515

@phaedrus: (znacznie później ...) dobre punkty, poprawiono przykład, aby alokator był ogólny, a przykład bardziej przejrzysty :-)
Evan Teran

163

W rzeczywistości przypadek użycia parametrów szablonu szablonu jest dość oczywisty. Gdy dowiesz się, że C ++ stdlib ma otwartą lukę w nieokreślaniu operatorów wyjściowych strumienia dla standardowych typów kontenerów, możesz napisać coś takiego:

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

Wtedy odkryłbyś, że kod dla wektora jest taki sam, dla forward_list jest w rzeczywistości taki sam, nawet dla wielu typów map jest nadal taki sam. Te klasy szablonów nie mają ze sobą nic wspólnego oprócz meta-interfejsu / protokołu, a użycie parametru szablonu szablonu pozwala uchwycić podobieństwo we wszystkich z nich. Przed przystąpieniem do pisania szablonu warto jednak sprawdzić odniesienie, aby przypomnieć, że kontenery sekwencji akceptują 2 argumenty szablonu - dla typu wartości i alokatora. Chociaż domyślnie jest przydzielany alokator, nadal powinniśmy uwzględnić jego istnienie w naszym operatorze szablonów <<:

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

Voila, która będzie działać automatycznie dla wszystkich obecnych i przyszłych kontenerów sekwencji zgodnych ze standardowym protokołem. Aby dodać mapy do miksu, należy zerknąć na odniesienie, aby zauważyć, że akceptują 4 parametry szablonu, więc potrzebowalibyśmy innej wersji operatora << z param szablonem 4-arg. Zobaczymy również, że std: pair próbuje być renderowany za pomocą operatora 2-arg << dla typów sekwencji, które wcześniej zdefiniowaliśmy, więc zapewnilibyśmy specjalizację tylko dla std :: pair.

Btw, z C + 11, który pozwala na szablony variadic (a zatem powinien pozwalać na argumenty szablonów variadic szablonów), możliwe byłoby posiadanie jednego operatora <<, aby rządzić nimi wszystkimi. Na przykład:

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

Wynik

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
To taki słodki przykład parametrów szablonu szablonu, ponieważ pokazuje przypadek, z którym wszyscy musieli sobie poradzić.
Ravenwater

3
To dla mnie najbardziej przebudzająca odpowiedź w szablonach C ++. @WhozCraig W jaki sposób uzyskałeś szczegóły rozszerzenia szablonu?
Arun

3
@Arun gcc obsługuje makro o nazwie __PRETTY_FUNCTION__, które między innymi zgłasza opisy parametrów szablonu w postaci zwykłego tekstu. clang też to robi. Czasami bardzo przydatna funkcja (jak widać).
WhozCraig

20
Parametr szablonu szablonu tutaj tak naprawdę nie dodaje żadnej wartości. Równie dobrze możesz użyć zwykłego parametru szablonu, jak każdej instancji szablonu klasy.
David Stone

9
Muszę się zgodzić z Davidem Stone'em. Nie ma tu sensu parametr szablonu szablonu. Stworzenie prostego szablonu (szablonu <typename Container>) byłoby znacznie prostsze i równie skuteczne. Wiem, że ten post jest dość stary, więc dodaję tylko 2 centy dla osób, które natkną się na tę odpowiedź i szukają informacji o szablonach szablonów.
Jim Vargo

67

Oto prosty przykład zaczerpnięty z „Modern C ++ Design - Generic Programming and Design Patterns Applied” autorstwa Andrei Alexandrescu:

Używa klas z parametrami szablonu szablonu w celu wdrożenia wzorca zasad:

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

Wyjaśnia: Zazwyczaj klasa hosta już zna lub może łatwo wywnioskować argument szablonu klasy strategii. W powyższym przykładzie WidgetManager zawsze zarządza obiektami typu Widget, więc wymaganie od użytkownika ponownego podania Widget w momencie tworzenia CreationPolicy jest zbędne i potencjalnie niebezpieczne. W takim przypadku kod biblioteki może wykorzystywać parametry szablonu szablonu do określania zasad.

W rezultacie kod klienta może używać „WidgetManager” w bardziej elegancki sposób:

typedef WidgetManager<MyCreationPolicy> MyWidgetMgr;

Zamiast bardziej nieporęcznego i podatnego na błędy sposobu, który wymagałaby definicja pozbawiona argumentów szablonu szablonu:

typedef WidgetManager< MyCreationPolicy<Widget> > MyWidgetMgr;

1
Pytanie konkretnie zadane dla przykładów innych niż wzorzec zasad.
user2913094,

Do tego pytania doszedłem właśnie z tej książki. Warto zauważyć, że parametry szablonów szablonów pojawiają się również w rozdziale Listy typów oraz w rozdziale Generowanie klas z listami typów .
Victor

18

Oto kolejny praktyczny przykład z mojej biblioteki sieci neuronowej CUDA Convolutional . Mam następujący szablon klasy:

template <class T> class Tensor

który faktycznie implementuje manipulację macierzami n-wymiarowymi. Istnieje również szablon klasy podrzędnej:

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

który implementuje tę samą funkcjonalność, ale w GPU. Oba szablony mogą współpracować ze wszystkimi podstawowymi typami, takimi jak float, double, int itp. Mam też szablon klasy (uproszczony):

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

Powodem tego jest składnia szablonu szablonu, ponieważ mogę zadeklarować implementację klasy

class CLayerCuda: public CLayerT<TensorGPU, float>

które będą miały zarówno wagi, jak i dane wejściowe typu float i na GPU, ale macierz połączeń będzie zawsze int, albo na CPU (poprzez określenie TT = Tensor), albo na GPU (przez określenie TT = TensorGPU).


Czy możesz wymusić odjęcie T za pomocą czegoś takiego jak: „szablon <klasa T, szablon <T> TT> CLayerT” i „klasa CLayerCuda: public CLayerT <TensorGPU <float>>”? Na wypadek gdybyś nie potrzebował TT <otherT>
NicoBerrogorry 18.04.18

NIGDY NIE UMYSŁ: szablon <szablon <klasa T> klasa U> klasa B1 {}; z ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/... z szybkiego wyszukiwania w Google
NicoBerrogorry

12

Załóżmy, że używasz CRTP w celu zapewnienia „interfejsu” dla zestawu szablonów potomnych; a zarówno element nadrzędny, jak i podrzędny są parametryczne w innych argumentach szablonu:

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;

Zwróć uwagę na powielenie „int”, które w rzeczywistości jest parametrem tego samego typu określonym dla obu szablonów. Możesz użyć szablonu dla DERIVED, aby uniknąć tego powielania:

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;

Zauważ, że eliminujesz bezpośrednio dostarczając inne parametry szablonu do szablonu pochodnego ; „interfejs” nadal je odbiera.

Pozwala to również budować typedefs w „interfejsie”, które zależą od parametrów typu, które będą dostępne z szablonu pochodnego.

Powyższy typedef nie działa, ponieważ nie można wpisać do nieokreślonego szablonu. Działa to jednak (a C ++ 11 ma natywną obsługę szablonów typedefs):

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

typedef typename derived_interface_type<int>::type derived_t;

Niestety potrzebujesz jednego typu pochodnej_interface dla każdej instancji szablonu pochodnego, chyba że jest jeszcze jedna sztuczka, której jeszcze się nie nauczyłem.


Potrzebowałem tego dokładnego rozwiązania dla jakiegoś kodu (dzięki!). Chociaż działa, nie rozumiem, w jaki sposób derivedmożna używać klasy szablonów bez argumentów szablonu, tj. Liniitypedef typename interface<derived, VALUE> type;
Carlton

@Carlton działa w zasadzie, ponieważ odpowiadający parametr szablonu, który jest wypełniany, jest zdefiniowany jako template <typename>. W pewnym sensie możesz myśleć o parametrach szablonu jako o „metatype”; normalnym typem metate dla parametru szablonu jest typenameto, że musi być wypełniony typem regularnym; te templateśrodki metatype musi być wypełniona odniesienia do szablonu. deriveddefiniuje szablon, który akceptuje jeden typenameparametr metatowany, więc pasuje do rachunku i można się do niego odwoływać tutaj. Ma sens?
Mark McKenna

C ++ 11 jeszcze wciąż typedef. Możesz także uniknąć duplikatu intw pierwszym przykładzie, używając standardowej konstrukcji, takiej jak value_typetypu DERIVED.
rubenvb,

Ta odpowiedź w rzeczywistości nie jest skierowana do C ++ 11; Odniosłem się do C ++ 11, żeby powiedzieć, że można obejść typedefproblem z bloku 2. Ale punkt 2 jest poprawny, myślę ... tak, prawdopodobnie byłby to prostszy sposób na zrobienie tego samego.
Mark McKenna,

7

Oto na co wpadłem:

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

};

Można rozwiązać:

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

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

class AInstance : A<B> //happy
{

};

lub (kod roboczy):

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

W rozwiązaniu z szablonami variadic dostarczonymi przez pfalcon trudno było mi faktycznie specjalizować operatora ostream dla std :: map ze względu na chciwość natury specjalizacji variadic. Oto niewielka zmiana, która zadziałała dla mnie:

#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

Oto jeden uogólniony z czegoś, czego właśnie użyłem. Publikuję go, ponieważ jest to bardzo prosty przykład i pokazuje praktyczny przypadek użycia wraz z domyślnymi argumentami:

#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

Poprawia to czytelność kodu, zapewnia dodatkowe bezpieczeństwo typów i oszczędza trochę wysiłku kompilatora.

Powiedzmy, że chcesz wydrukować każdy element kontenera, możesz użyć następującego kodu bez parametru szablonu szablonu

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

lub z parametrem szablonu szablonu

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

Załóżmy, że przekazujesz liczbę całkowitą print_container(3). W pierwszym przypadku szablon zostanie utworzony przez kompilator, który narzeka na użyciec w pętli for, ten drugi w ogóle nie utworzy szablonu, ponieważ nie można znaleźć pasującego typu.

Ogólnie rzecz biorąc, jeśli klasa / funkcja szablonu została zaprojektowana do obsługi klasy szablonu jako parametru szablonu, lepiej jest to wyjaśnić.


1

Używam go dla typów wersjonowanych.

Jeśli masz typ wersjonowany za pomocą szablonu, takiego jak MyType<version>, możesz napisać funkcję, w której możesz przechwycić numer wersji:

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

Możesz więc robić różne rzeczy w zależności od przekazywanej wersji typu, zamiast przeciążania każdego typu. Możesz również mieć funkcje konwersji, które przyjmują MyType<Version>i zwracają MyType<Version+1>, w ogólny sposób, a nawet ponownie je skonfigurować, aby miały ToNewest()funkcję, która zwraca najnowszą wersję typu z dowolnej starszej wersji (bardzo przydatne dla dzienników, które mogły być przechowywane jakiś czas temu ale muszą zostać przetworzone za pomocą najnowszego narzędzia).

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.