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.