C++11 wprowadził szablon wariadyczny (ang. a variadic template), który potrafi przyjąć dowolną liczbę argumentów szablonu, także zerową. Szablon wariadyczny jest mechanizmem czasu kompilacji, który jest konkretyzowany w miejscu użycia.
Szablon wariadyczny poznamy po trójkropku ...
w liście parametrów
szablonu:
#include <iostream>
using namespace std;
template <typename... P>
void foo()
{
cout << __PRETTY_FUNCTION__ << endl;
cout << sizeof...(P) << endl;
}
int
main()
{
foo<>();
foo<int>();
foo<bool, double>();
}
W powyższym przykładzie trójkropek został użyty w definicji paczki parametrów, a potem w rozwinięciu tej paczki.
Trójkropek jest częścią definicji paczki parametrów (ang. a parameter pack), po którym następuje nazwa paczki. Paczka parametrów szablonu może być szablonu albo funkcji. Paczka parametrów jest używana w rozwinięciu albo wyrażeniu złożenia.
Parametry szablonu w paczce są tego samego rodzaju: w przykładzie wyżej są typowe, a w przykładzie niżej wartościowe.
#include <iostream>
using namespace std;
template <unsigned... N>
void foo()
{
cout << __PRETTY_FUNCTION__ << endl;
cout << sizeof...(N) << endl;
}
int
main()
{
foo<>();
foo<1>();
foo<1, 2>();
// foo<1, -1>();
}
Nazwę paczki z następującym trójkropkiem nazywa się rozwinięciem paczki (ang. pack expansion). Paczka parametrów szablonu jest rozwijana do listy argumentów szablonu oddzielonych przecinkami:
#include <iostream>
#include <string>
struct A
{
};
template <typename... P>
struct B: P...
{
B()
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
};
int
main()
{
// Expands to a structure type that does not have a base class.
B<>();
// Expands to a structure type that has the base class A.
B<A>();
// Expands to a structure type derived from A and string.
B<A, std::string>();
}
W przykładach wyżej argumenty szablonu musiały być jawnie podane, bo funkcje i konstruktor nie miały parametrów i argumentów wywołania, na podstawie których argumenty szablonu mogły być wywnioskowane. Argumenty dla parametrów szablonu w paczce są wnioskowane na podstawie definicji paczki parametrów funkcji i argumentów wywołania funkcji.
Paczka parametrów funkcji jest definiowana w liście parametrów funkcji także z użyciem trójkropka: pierwsze podajemy nazwę paczki parametrów szablonu, trójkropek, a następnie nazwę paczki parametrów funkcji. Co ciekawe, definicja paczki parametrów funkcji jest jednocześnie rozwinięciem paczki parametrów szablonu.
#include <iostream>
using namespace std;
template <typename... P>
void foo(P... p)
{
cout << __PRETTY_FUNCTION__ << endl;
}
int
main()
{
foo();
foo(1);
foo(1, 2.0);
}
W przykładzie wyżej funkcja przyjmuje argumenty przez wartość. Może
też przyjmować przez referencję stałą, jeżeli paczkę zdefiniujemy jako
const Args &... args
.
Rozwinięcie paczki parametrów funkcji zapisujemy jako nazwa paczki z następującym trójkropkiem.
#include <iostream>
#include <string>
#include <vector>
template <typename T, typename... P>
auto
factory(P... p)
{
return T{p...};
}
int
main()
{
std::cout << factory<std::string>("Hello!") << std::endl;
auto p = factory<std::vector<int>>(1, 2, 3);
}
Paczkę parametrów szablonu można rozwijać w zgraniu (ang. lockstep) z paczką parametrów funkcji, np. podczas inicjalizacji obiektów bazowych parametrami konstruktora klasy wyprowadzonej:
#include <iostream>
#include <string>
struct A
{
A() = default;
A(int) {}
};
template <typename... P>
struct B: P...
{
B(const P &... p): P(p)...
{
std::cout << __PRETTY_FUNCTION__ << std::endl;
}
};
int
main()
{
B<>();
B<A>(1);
B<A, std::string>({}, "Hello!");
}
Parametry paczki funkcji możemy przetwarzać rekurencyjnie. Sztuczka polega na zdefiniowaniu dwóch parametrów funkcji: pierwszy do przetworzenia przez funkcję, a drugi jako paczka do przekazania jako argument wywołania rekurencyjnego. W ten sposób zmniejszamy liczbę parametrów paczki funkcji o jeden za każdym wywołaniem rekurencyjnym.
#include <iostream>
#include <string>
template <typename P1, typename... P>
void
print(P1 p1, P... p)
{
std::cout << p1;
if constexpr(sizeof...(P))
print(p...);
}
int
main()
{
print("Hello", ' ', std::string("World"), " x ", 100, '\n');
}
Wyrażenie złożenia (ang. a fold expression, od C++17) jest przepisem
na wygenerowanie wyrażenia z użyciem dowolnego dwuargumentowego
operatora op
i paczki parametrów. Wyrażenie tak się nazywa, bo
składa docelowe wyrażenie (to, które sami napisalibyśmy “ręcznie”) do
skompresowanego zapisu. Wyrażenie złożenia jest konkretyzowane w
czasie kompilacji dla danej paczki parametrów, co eliminuje potrzebę
przetwarzania rekurencyjnego. Wyrażenie złożenia poznajemy po ...
i
nawiasach. Są cztery wersje: dwie jednoargumentowe i dwie
dwuargumentowe, ale ciągle z użyciem tego samego operatora op
.
Paczka p
składa się z parametrów p1, p2, …,
p(n-1), pn. Wyrażenie złożenia wymaga wyrażenia
E
, które używa paczki p
. Wyrażenie E
opracowane dla parametru
pi zapisujemy jako Ei.
Wersje jednoargumentowe wyrażenia złożenia, gdzie argumentem jest E
:
wersja lewa: (... op E)
-> ((E1 op E2) op …)
wersja prawa: (E op ...)
-> (… op (E(n-1) op En))
Wersja lewa przetwarza parametry paczki od lewej strony (do prawej, czyli od p1 do pn), a prawa od prawej (do lewej, czyli od pn do p1). Zatem wersja lewa przetwarza argumenty tak, jakby operator miał wiązanie lewe, a prawa tak, jakby miał wiązanie prawe.
Dla działania łącznego (np. dodawania) nie ma znaczenia, czy przetwarzamy od lewej czy od prawej strony, więc oba wyrażenia złożenia (lewe i prawe) zwrócą ten sam wynik. Jeżeli jednak działanie nie jest łączne, to trzeba wybrać właściwą wersję wyrażenia. W przykładzie niżej odejmowanie nie jest łączne i ma wiązanie lewe, więc powinniśmy użyć lewego wyrażenia złożenia.
template <int... P>
constexpr int left()
{
return (... - P);
}
template <int... P>
constexpr int right()
{
return (P - ...);
}
int main()
{
// (3 - 2) - 1 = 0
static_assert(left<3, 2, 1>() == 0);
// 3 - (2 - 1) = 2
static_assert(right<3, 2, 1>() == 2);
}
Przykład poniżej pokazuje konieczność użycia prawego wyrażenia:
#include <cassert>
template <typename... T>
void left(T &... p)
{
(... += p);
}
template <typename... T>
void right(T &... p)
{
(p += ...);
}
int main()
{
int x = 1, y = 2, z = 3;
// We would like to inrement y by z, and then x by y:
//
// x += y += z;
//
// The above compiles and works as expected, because += has the
// right associativity, and so we need the right fold expression.
// left(x, y, z);
right(x, y, z);
assert(x == 6);
assert(y == 5);
assert(z == 3);
}
Wersje dwuargumentowe wymagają drugiego argumentu, którym jest
wyrażenie inicjalizujące A
.
wersja lewa (A op ... op E)
-> ((A op E1) op …)
wersja prawa (E op ... op A)
-> (… op (En op A))
Strumienie wejścia-wyjścia są często wyrażeniem inicjalizującym dwuargumentowego wyrażenia złożenia, jak w przykładzie niżej. Wyrażenie musi być lewe, bo:
pierwszym argumentem operatora przekierowania (>>
czy <<
) jest
strumień (wejściowy czy wyjściowy),
operator przekierowania zwraca strumień, który otrzymał jako pierwszy argument,
strumień zwracany przez wyrażenie przekierowania staje się pierwszym argumentem kolejnego wyrażenia przekierowania.
#include <iostream>
#include <sstream>
#include <string>
#include <utility>
using namespace std;
template <typename... P>
void
read(P &... p)
{
(cin >> ... >> p);
// (p >> ... >> cin);
}
template <typename... P>
void
write(P &&... p)
{
(cout << ... << p);
// (p << ... << cout);
}
int
main()
{
write("Hello", ' ', std::string("World"), " x ", 100, "!\n");
string txt;
int x;
bool b;
read(txt, x, b);
write(txt, ' ', x, ' ', b, '\n');
}
Oto przykład z prawym wyrażeniem:
#include <cassert>
template <typename R, typename... T>
void left(R &r, T &... p)
{
(r += ... += p);
}
template <typename R, typename... T>
void right(R &r, T &... p)
{
(p += ... += r);
}
int main()
{
int x = 1, y = 2, z = 3;
// We would like to inrement y by z, and then x by y:
//
// x += y += z;
//
// The above compiles and works as expected, because += has the
// right associativity, and so we need the right fold expression.
// left(x, y, z);
right(x, y, z);
assert(x == 6);
assert(y == 5);
assert(z == 3);
}
Jeżeli dodalibyśmy wyrażenie A
na początek paczki, to moglibyśmy
skorzystać z jednoargumentowego wyrażenia złożenia, ale byłoby to
niewygodne i mniej ekspresywne, jak pokazano niżej:
#include <iostream>
#include <sstream>
#include <string>
#include <utility>
using namespace std;
template <typename... P>
void
read(P &&... p)
{
(... >> p);
}
template <typename... P>
void
write(P &&... p)
{
(... << p);
}
int
main()
{
write(cout, "Hello", ' ', std::string("World"), " x ", 100, "!\n");
istringstream in("Hi! 100 0");
string txt;
bool b;
// We don't care about the 100, so we read it into a temporary.
read(in, txt, int(), b);
write(cout, txt, ' ', 200, ' ', b, '\n');
}
Przykład niżej używa operatora przecinka, który jest nietypowy,
ponieważ łączy dwa niezależne wyrażenia. Łączone wyrażenia nie mają
ze sobą nic wspólnego i są opracowywane niezależnie. Przecinek
gwarantuje jedynie, że pierwsze będzie wykonane pierwsze wyrażenie,
zatem nawiasy z drugiego wyrażenia nie mogą mieć wpływu na kolejność
wykonania pierwszego wyrażenia. W poniższym przykładzie, nawiasowanie
nie ma wpływu na wynik, ponieważ kolejność opracowania wyrażeń z
operatorem <<
ustalają przecinki.
#include <iostream>
using namespace std;
int main()
{
(cout << '1', cout << '2'), cout << '3';
cout << '1', (cout << '2', cout << '3');
}
Przykład niżej używa wersji lewej, gdzie E
to (std::cout << ", " <<
p)
a operatorem jest przecinek. Jeżeli paczka p
jest pusta, to
wyrażenie złożenia jest puste. Jeżeli p
ma jeden parametr, to
kompilator dokooptowuje dodatkowy pusty parametr, jeżeli taki istnieje
(dla operatora ,
jest nim void()
), bo op
wymaga dwóch
argumentów. Żeby na początku nie wypisać samego przecinka, to
pierwszy parametr wypisujemy poza paczką (tak, jak w przetwarzaniu
rekurencyjnym), a wypisanie każdego parametru z paczki jest
poprzedzone wypisaniem przecinka.
#include <iostream>
#include <string>
template <typename T, typename... P>
void
print(const T &t, const P &... p)
{
std::cout << t;
(... , (std::cout << ", " << p));
}
int
main()
{
print(5, "10", std::string("15"));
std::cout << std::endl;
// What's this?
1, void();
}
Szablon wariadyczny przyjmuje dowolną liczbę argumentów.
Paczka parametrów jest używana w rozwinięciach albo w wyrażeniach złożenia.
Wariadyczne mogą być listy:
dziedziczenia i inicjalizacji,
parametrów szablonu i argumentów szablonu,
parametrów funkcji i argumentów funkcji.
Czy paczka parametrów szablonu może przyjąć argumenty różnego rodzaju?
Czy paczka parametrów może być pusta?
Co to jest wyrażenie złożenia?