Variadic templates#
W C++11 szablony mogą akceptować dowolną ilość (również zero) parametrów. Jest to możliwe dzięki użyciu specjalnego grupowego parametru szablonu tzw. parameter pack, który reprezentuje wiele lub zero parametrów szablonu.
Parameter pack#
Parameter pack może być
grupą parametrów szablonu
template<typename... Ts> // template parameter pack class tuple { //... }; tuple<int, double, string&> t1; // 3 arguments: int, double, string& tuple<> empty_tuple; // 0 arguments
grupą argumentów funkcji szablonowej
template <typename T, typename... Args> shared_ptr<T> make_shared(Args&&... params) { //... } auto sptr = make_shared<Gadget>(10); // Args as template param: int // Args as function param: int&&
Rozpakowanie paczki parametrów#
Podstawową operacją wykonywaną na grupie parametrów szablonu jest rozpakowanie jej za pomocą operatora ... (tzw. pack expansion).
template <typaname... Ts> // template parameter pack
struct X
{
tuple<Ts...> data; // pack expansion
};
Rozpakowanie paczki parametrów (pack expansion) może zostać zrealizowane przy pomocy wzorca zakończonego elipsą ...:
template <typaname... Ts> // template parameter pack
struct XPtrs
{
tuple<Ts const*...> ptrs; // pack expansion
};
XPtrs<int, string, double> ptrs; // contains tuple<int const*, string const*, double const*>
Najczęstszym przypadkiem użycia wzorca przy rozpakowaniu paczki parametrów jest implementacja perfect forwarding’u:
template <typename... Args>
void make_call(Args&&... params)
{
callable(forward<Args>(params)...);
}
Idiom Head/Tail#
Praktyczne użycie variadic templates wykorzystuje często idiom Head/Tail (znany również First/Rest).
Idiom ten polega na zdefiniowaniu wersji szablonu akceptującego dwa parametry:
- pierwszego Head
- drugiego Tail w postaci paczki parametrów
W implementacji wykorzystany jest parametr (lub argument) typu Head, po czym rekursywnie wywołana jest implementacja dla rozpakowanej
paczki parametrów typu Tail.
Dla szablonów klas idiom wykorzystuje specjalizację częściową i szczegółową (do przerwania rekurencji):
template <typename... Types>
struct Count;
template <typename Head, typename... Tail>
struct Count<Head, Tail...>
{
constexpr static int value = 1 + Count<Tail...>::value; // expansion pack
};
template <>
struct Count<>
{
constexpr static int value = 0;
};
//...
static_assert(Count<int, double, string&>::value == 3, "must be 3");
W przypadku szablonów funkcji rekurencja może być przerwana przez dostarczenie odpowiednio przeciążonej funkcji. Zostanie ona w odpowiednim momencie rozwijania rekurencji wywołana.
void print()
{}
template <typename T, typename... Tail>
void print(const T& arg1, const Tail&... params)
{
cout << arg1 << endl;
print(params...); // function parameter pack expansion
}
Operator sizeof…#
Operator sizeof... umożliwia odczytanie na etapie kompilacji ilości parametrów w grupie.
template <typename... Types>
struct VerifyCount
{
static_assert(Count<Types...>::value == sizeof...(Types),
"Error in counting number of parameters");
};
Forwardowanie wywołań funkcji#
Variadic templates są niezwykle przydatne do forwardowania wywołań funkcji.
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... params)
{
return std::unique_ptr<T>(new T(forward<Args>(params)...);
}
Ograniczenia paczek parametrów#
Klasa szablonowa może mieć tylko jedną paczkę parametrów i musi ona zostać umieszczona na końcu listy parametrów szablonu:
template <size_t... Indexes, typename... Ts> // error
class Error;
Można obejść to ograniczenie w następujący sposób:
template <size_t... Indexes> struct IndexSequence {};
template <typename Indexes, typename Ts...>
class Ok;
Ok<IndexSequence<1, 2, 3>, int, char, double> ok;
Funkcje szablonowe mogą mieć więcej paczek parametrów:
template <int... Factors, typename... Ts>
void scale_and_print(Ts const&... args)
{
print(ints * args...);
}
scale_and_print<1, 2, 3>(3.14, 2, 3.0f); // calls print(1 * 3.14, 2 * 2, 3 * 3.0)
Uwaga! Wszystkie paczki w tym samym wyrażeniu rozpakowującym muszą mieć taki sam rozmiar.
scale_and_print<1, 2>(3.14, 2, 3.0f); // error
“Nietypowe” paczki parametrów#
Podobnie jak w przypadku innych parametrów szablonów, paczka parametrów nie musi być paczką typów, lecz może być paczką stałych znanych na etapie kompilacji
template <size_t... Values>
struct MaxValue; // primary template declaration
template <size_t First, size_t... Rest>
struct MaxValue<First, Rest...>
{
static constexpr size_t rvalue = MaxValue<Rest...>::value;
static constexpr size_t value = (First < rvalue) ? rvalue : First;
};
template <size_t Last>
struct MaxValue<Last>
{
static constexpr size_t value = Last; // termination of recursive expansion
};
static_assert(MaxValue<1, 5345, 3, 453, 645, 13>::value == 5345, "Error");
Variadic Mixins#
Variadic templates mogą być skutecznie wykorzystane do implementacji klas mixin
#include <vector>
#include <string>
template <typename... Mixins>
class X : public Mixins...
{
public:
X(Mixins&&... mixins) : Mixins(mixins)...
{}
};
Klasa taka może zostać wykorzystana później w następujący sposób:
X<std::vector<int>, std::string> x({ 1, 2, 3 }, "text");
x.std::string::size();
(unsigned long) 4
x.std::vector<int>::size();
(unsigned long) 3
Curiously-Recurring Template Parameter (CRTP)#
Ciekawym sposobem użycia variadic templates jest implementacja znanego idiomu CRTP.
Zaimplementujmy najpierw klasę licznika obiektów:
template <typename T>
class Counter
{
public:
Counter() { ++counter_; }
~Counter() { --counter_; }
static size_t count() { return counter_; }
private:
static size_t counter_;
};
template <typename T> size_t Counter<T>::counter_;
Klasa Counter może zostać wielokrotnie użyta do implementacji
zliczania obiektów za pomocą idiomu CRTP. Dziedzicząc typ T po
Counter<T> wstrzykujemy do T implementację zliczania obiektów.
class Thing : public Counter<Thing> // OK
{
};
Thing thing1, thing2;
{
Thing thing3;
}
Thing::count();
(unsigned long) 2
Innym praktycznym zastosowaniem CRTP jest implementacja operatorów porównań dla klas.
Operatory
==oraz!=mogą zostać zaimplementowane za pomocą funkcji pomocniczejequal_to().Operatory
<,<=,>,>=mogą zostać zaimplementowane za pomocą funkcjiequal_to(),less_than()orazgreater_than()Klasy szablonowe
Eq<T>orazRel<T>implementują generyczne operatory porównań wykorzystujące wyżej wymienione funkcje pomocnicze (operatory te są zadeklarowane jako zaprzyjaźnione -friend)
template <typename T>
class Eq
{
friend bool operator==(T const& a, T const& b) { return a.equal_to(b); }
friend bool operator!=(T const& a, T const& b) { return !a.equal_to(b); }
};
template <typename T>
class Rel
{
friend bool operator<(T const& a, T const& b) { return a.less_than(b); }
friend bool operator<=(T const& a, T const& b) { return !b.less_than(a); }
friend bool operator>(T const& a, T const& b) { return b.less_than(a); }
friend bool operator>=(T const& a, T const& b) { return !a.less_than(b); }
};
struct AnotherThing : public Eq<AnotherThing>
{
int value;
AnotherThing(int value) : value{value}
{}
bool equal_to(AnotherThing const& other) const
{
return value == other.value;
}
};
AnotherThing at1{1};
AnotherThing at2{2};
(at1 != at2);
(bool) true
(at1 == at2);
(bool) false
Użycie szablonu jako parametru szablonu upraszcza stosowanie CRTP
template <template <typename> class CRTP>
struct OtherThing : public CRTP<OtherThing<CRTP>>
{
int value;
OtherThing(int value = 0) : value{value}
{}
bool equal_to(OtherThing const& other) const
{
return value == other.value;
}
bool less_than(OtherThing const& other) const
{
return value < other.value;
}
};
OtherThing<Counter> counted_thing;
OtherThing<Eq> equality_comparable_thing;
OtherThing<Rel> relational_thing;
Jeżeli chcemy użyć wielu implementacji CRTP (np.
Thing<Counter, Eq>) dla jednej klasy musimy wykorzystać variadic
templates:
template <template <typename> class... CRTPs>
struct SuperThing : public CRTPs<SuperThing<CRTPs...>>...
{
int value;
SuperThing(int value = 0) : value{value}
{}
bool equal_to(SuperThing const& other) const
{
return value == other.value;
}
bool less_than(SuperThing const& other) const
{
return value < other.value;
}
};
SuperThing<Counter, Eq, Rel> a_thing{10};
SuperThing<Counter, Eq, Rel> b_thing{20};
a_thing.count();
std::cout << (a_thing < b_thing) << std::endl;
W implementacji klasy szablonowej SuperThing:
SuperThingjest nazwą szablonuCRTPsjest szablonową paczką parametrów szablonu (template template parameter pack)SuperThing<CRTPs...>jest nazwą typuCRTPs<SuperThing<CRTPS...>>jest wzorcem paczki parametrówCRTPs<SuperThing<CRTPS...>>...jest rozwinięciem wzorcapublic CRTPs<SuperThing<CRTPs...>>...jest listą klas bazowych