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 pomocniczej equal_to().

  • Operatory <, <=, >, >= mogą zostać zaimplementowane za pomocą funkcji equal_to(), less_than() oraz greater_than()

  • Klasy szablonowe Eq<T> oraz Rel<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:

  • SuperThing jest nazwą szablonu

  • CRTPs jest szablonową paczką parametrów szablonu (template template parameter pack)

  • SuperThing<CRTPs...> jest nazwą typu

  • CRTPs<SuperThing<CRTPS...>> jest wzorcem paczki parametrów

  • CRTPs<SuperThing<CRTPS...>>... jest rozwinięciem wzorca

  • public CRTPs<SuperThing<CRTPs...>>... jest listą klas bazowych