A variadikus sablonok (variadic templates) teszik lehetővé, hogy egy függvény- vagy osztálysablon tetszőleges számú típusparamétert és argumentumot fogadjon el úgy, hogy a típusinformáció fordítási időben megmaradjon. A C++11-ben bevezetett parameter pack és pack expansion, a C++17-es fold expression, valamint a C++20-as constraint-ek együtt teszik lehetővé a típusos, mégis rugalmas interfészeket. Ez a rész a pack kibontásának mechanizmusát, a fold kiértékelését és az argumentumok élettartamára vonatkozó következményeket tárgyalja. A forwarding reference és a perfect forwarding mechanizmusát a Forwarding reference és perfect forwarding rész tárgyalta; a constraint-ekre a Concepts, requires és constrained overload rész épít. Itt ezekre csak a szükséges mértékben hivatkozunk.
A szabványban a variadikus sablonok és a pack expansion a
[temp.variadic], a fold expression a
[expr.prim.fold] alatt található. Ezek a
paragrafushivatkozások tájékoztatók; a konkrét C++20 szabvány szövegében
ellenőrizendők. A példák C++20 nyelvi módot és a standard library
C++17/20 elemeit használják; a buildparancsok feltételezik a C++20
támogatását. A bemutatott kimenetek illusztrációk, nem mért
eredmények.
A parameter pack a sablon típusparamétereinek vagy
függvényparamétereinek olyan csoportja, amelyet a ...
jelöl. Két alapvető alakja van:
template <class... Ts> // típusparameter-pack
void f(Ts... args); // függvényparameter-pack
template <class... Ts>
void g(const Ts&... args); // a minta: const Ts&A Ts... nem egyetlen típus, hanem nulla vagy több típus
szintaktikai csoportja. A args... hasonlóképpen nulla vagy
több függvényparaméter. A pack mérete a sizeof...(Ts) vagy
sizeof...(args) kifejezéssel kérdezhető le; ez fordítási
időben konstans.
A pack expansion olyan szintaktikai minta, amelyben
egy pack neve szerepel, és amelyet a compiler a pack minden elemére
egyszer kifejt. A minta a ... előtti egység. Például a
const Ts&... mintában a minta
const Ts&, így minden Ts elemre egy
const T& paraméter jön létre. A
std::forward<Args>(args)... mintában a minta
std::forward<Args>(args), így minden
Args/args párra egy-egy
std::forward hívás keletkezik.
A pack expansion érvényességének feltételei:
f(args..., rest...) csak akkor érvényes,
ha a két pack elemszáma azonos.A pack nem futásidejű tömb. Nem lehet Ts típusú változót
deklarálni, és nem lehet indexelni. A pack elemei csak kifejtve, például
fold expressionnel, rekurzióval vagy
std::index_sequence-szel érhetők el.
A fold expression (C++17) a pack expansion egy olyan formája, amely a pack elemeit egy bináris operátorral kombinálja. Négy szintaktikai alak létezik:
Itt E a packot tartalmazó minta, I pedig
egy inicializáló kifejezés. A zárójelezés levezetése (vázlat):
(E op ...) jobbra zárójelez:
E1 op (E2 op (... op EN)).(... op E) balra zárójelez:
((E1 op E2) op ...) op EN.(E op ... op I) jobbra zárójelez:
E1 op (E2 op (... op (EN op I))).(I op ... op E) balra zárójelez:
((I op E1) op E2) ... op EN.A zárójelezés determinisztikus, de ez nem jelenti azt, hogy az operandusok kiértékelési sorrendje is mindig rögzített; erre a következő alfejezetekben visszatérünk.
A fold expression csak a megengedett operátorokkal használható. Ide
tartoznak többek között a +, -,
*, /, %, &,
|, ^, <<,
>>, &&, ||,
,, az összehasonlító operátorok és a hozzárendelő
operátorok. A <=> nem szerepel a fold operátorai
között, ezért spaceship operátorral nem foldolhatunk.
Az üres pack kezelése operátorfüggő. Unary fold üres packra csak három operátornál érvényes ([C++20] [temp.variadic]/9):
(args && ...) üres packra
true,(args || ...) üres packra false,(args, ...) üres packra void().Minden más operátornál — a + és a * esetén
is — az üres pack unary folddal fordítási hiba; a szabvány nem ad
0-t vagy 1-et. Binary foldnál az
I inicializáló érték kötelező, és üres pack esetén az
eredmény maga az I; ezért ha üres packot is támogatni kell,
a kezdőértékes (0 + ... + args) és
(1 * ... * args) a helyes alak.
// fold_empty.cpp — C++20: üres pack és fold
template <class... Ts> constexpr auto sum(Ts... xs) { return (0 + ... + xs); }
template <class... Ts> constexpr auto product(Ts... xs) { return (1 * ... * xs); }
template <class... Ts> constexpr bool all(Ts... xs) { return (xs && ...); }
template <class... Ts> constexpr auto sum_unary(Ts... xs) { return (xs + ...); }
static_assert(sum() == 0 && sum(1, 2, 3) == 6);
static_assert(product() == 1 && product(2, 3, 4) == 24);
static_assert(all() && all(true, true) && !all(true, false));
static_assert(sum_unary(1, 2) == 3);
#ifdef EMPTY_UNARY
static_assert(sum_unary() == 0); // hiba: üres pack unary + folddal
#endif
int main() {}Buildjegyzőkönyv (lab-gcc profil).
g++ -std=c++20 -Wall -Wextra -Wpedantic fold_empty.cpp
figyelmeztetés nélkül fordul; -DEMPTY_UNARY mellett a
sum_unary()
fold of empty expansion over operator+ hibát ad.
Fontos példa a rövidzárlatra és az asszociativitásra:
template <class... Ts>
bool all_positive(Ts... xs) {
return (true && ... && (xs > 0));
}Ez bal oldali binary fold:
((true && (x1 > 0)) && (x2 > 0)) && ....
Az üres pack eredménye true. A &&
rövidzárlatos, így ha egy korábbi elem hamis, a későbbi
xs > 0 kifejezések nem értékelődnek ki. Ugyanez
|| esetén is igaz. A + operátor nem
rövidzárlatos, és az operandusok kiértékelési sorrendje sem
garantált.
A std::index_sequence<Is...> (C++14) egy olyan
típus, amelynek template argumentumlistája 0, 1, ..., N-1.
A std::make_index_sequence<N> és a
std::index_sequence_for<Ts...> segítségével
generálhatunk indexsorozatot. A tuple elemeihez compile-time indexkel, a
std::get<Is>(t) hívással férünk hozzá.
template <class Tuple, std::size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}
template <class... Ts>
void print_tuple(const std::tuple<Ts...>& t) {
print_tuple_impl(t, std::index_sequence_for<Ts...>{});
}A print_tuple_impl második paramétere névtelen, mert
csak a típusából nyerjük ki az Is... packot. A
std::get<Is>(t) a t const lvalue
referenciáján keresztül const referenciát ad az adott elemre. A
std::get indexe compile-time ellenőrzött: ha
Is >= sizeof...(Ts), a program nem fordul. A
std::apply (C++17) ugyanezt a mintát használja, de
std::invoke-on keresztül hívja a callable-t, így
tagfüggvénypointereket is kezel.
A tuple indexelésnél a value category számít. Ha t
lvalue, a std::get<Is>(t) lvalue referenciát ad; ha
t rvalue, akkor rvalue referenciát. A
std::apply(f, t) a t value category-ját
továbbítja, így a callable megkaphatja a tuple elemeit a megfelelő value
category-vel.
A
template <class... Args> void f(Args&&... args)
egy forwarding reference pack. Minden Args
a hívás argumentumából dedukálódik, és a value category megőrzéséhez a
std::forward<Args>(args)... szükséges:
template <class... Args>
void forward_to_sink(Args&&... args) {
sink(std::forward<Args>(args)...);
}A std::forward<Args>(args)... mintája minden
argumentumra a saját Args-át használja, így az lvalue
argumentum lvalue marad, az rvalue argumentum rvalue marad. A
std::move(args)... ezzel szemben feltétel nélkül rvalue-vé
tesz, és elveszíti az lvalue/rvalue különbséget; forwarding packnél ez
hibás.
Az argumentum élettartama szempontjából kulcsfontosságú, hogy a
forwarding reference nem hosszabbítja meg a temporary élettartamát a
függvényhíváson túl. A temporary a teljes kifejezés végéig él, így a
forward_to_sink hívása alatt a sink számára is
érvényes. Ha azonban a packot eltároljuk, már nem hivatkozhatunk a
temporary-re:
template <class... Args>
auto make_refs(Args&&... args) {
return std::tuple<Args&&...>(std::forward<Args>(args)...);
}
auto t = make_refs(std::string("x")); // a temporary a teljes kifejezés végén megszűnik
// std::get<0>(t) használata érvénytelen élettartamra hivatkozikItt a std::string("x") temporary a teljes kifejezés
végén megszűnik, a t azonban rvalue referenciát tárol rá. A
t létrehozása önmagában nem UB, de a
std::get<0>(t) használata dangling referencián
keresztül történik, ami nem definiált viselkedés. A tulajdonos ebben az
esetben nem a t, hanem a temporary volt; a temporary
megszűnése invalidálja a referenciát.
A biztonságos tároláshoz a referenciákat értékekké kell alakítani:
template <class... Args>
auto make_owned(Args&&... args) {
return std::make_tuple(std::forward<Args>(args)...);
}A std::make_tuple a
std::decay_t<Args>... típusokat tárolja, így a
temporary értéke bemásolódik vagy mozgatódik, és a visszatérő tuple
független a bemeneti temporary élettartamától. Ugyanezt teszi a
std::tuple<std::decay_t<Args>...> is. A
const Ts&... pack olvasásra biztonságos a híváson
belül, de ha a referenciákat eltároljuk, ugyanígy dangling veszély áll
fenn.
A fold zárójelezése determinisztikus, de az operandusok kiértékelési
sorrendje az operátor szabályaitól függ. A , operátor
balról jobbra értékel, és a bal operandus értéke után a jobb operandus
értékelődik; a && és || rövidzárlatos
és sorrendezett. A +, *, <<
és hasonló operátorok esetén az operandusok kiértékelési sorrendje nem
specifikált, még akkor sem, ha a fold zárójelezése rögzített.
Ezért a logger példában a , foldot használjuk, ha a
kiírás sorrendje számít:
template <class... Args>
void log_line(std::string_view level, const Args&... args) {
std::cout << '[' << level << "] ";
((std::cout << args << ' '), ...);
std::cout << '\n';
}A ((std::cout << args << ' '), ...) unary
jobbra zárójelező fold a , operátorral. A ,
balról jobbra értékel, így a std::cout kiírások sorrendje
garantált. Egy (f(args) + ...) fold esetén az
f hívások sorrendje nem garantált, még akkor sem, ha a
+ zárójelezése balról jobbra történik. A
&& és || foldoknál a rövidzárlat miatt
a későbbi elemek ki sem értékelődhetnek; ez akkor fontos, ha a minta
mellékhatással jár.
A C++11-es variadikus sablonok tipikus feldolgozása rekurzióval történt:
template <class T>
void print(const T& x) { std::cout << x; }
template <class T, class... Rest>
void print(const T& x, const Rest&... rest) {
std::cout << x << ' ';
print(rest...);
}Ez lineáris példányosítási mélységet okoz: minden argumentumhoz egy
új print példány jön létre, és a rekurzió mélysége az
argumentumok számával nő. Nagy pack esetén a compiler template-rekurziós
limitjébe ütközhetünk (GCC/Clang esetén például a
-ftemplate-depth értéke korlátozhat).
A fold expression ugyanezt egyetlen kifejezéssel fejezi ki:
template <class... Ts>
void print(const Ts&... xs) {
((std::cout << xs << ' '), ...);
}A fold elkerüli a rekurziós mélységet, de a kifejezés mérete továbbra
is lineáris a pack elemszámában. A compile-time költség nem feltétlenül
kisebb; a példányosított kifejezés mérete és a compiler optimalizálási
munkája compiler- és kódfüggő. A std::index_sequence
használata szintén lineáris példányosítást eredményez, de gyakran
áttekinthetőbb, mint a rekurzió. Mérést nem állítunk; a választás a
konkrét fordító és kódbázis tapasztalata alapján történik.
Az alábbi példa a log_line függvényt
const Args&... packkal, a tuple kiíratását pedig
std::index_sequence-szel valósítja meg. A kód C++20-ban
fordul, GCC-vel vagy Clang-gel.
// variadic_logger.cpp
#include <cstddef>
#include <iostream>
#include <string>
#include <string_view>
#include <tuple>
#include <utility>
template <class... Args>
void log_line(std::string_view level, const Args&... args) {
std::cout << '[' << level << "] ";
((std::cout << args << ' '), ...);
std::cout << '\n';
}
template <class Tuple, std::size_t... Is>
void print_tuple_impl(const Tuple& t, std::index_sequence<Is...>) {
((std::cout << (Is == 0 ? "" : ", ") << std::get<Is>(t)), ...);
}
template <class... Ts>
void print_tuple(const std::tuple<Ts...>& t) {
print_tuple_impl(t, std::index_sequence_for<Ts...>{});
}
int main() {
log_line("INFO", "start", 42, 3.5);
auto t = std::make_tuple(std::string("db"), 5432, true);
print_tuple(t);
std::cout << '\n';
}A buildparancs:
g++ -std=c++20 -Wall -Wextra -Wpedantic -O2 variadic_logger.cpp -o variadic_logger
./variadic_loggerClang esetén ugyanez clang++-szal. A várható kimenet
illusztráció:
[INFO] start 42 3.5
db, 5432, 1
A log_line a const Args&... packot
használja, mert csak olvas; a temporary argumentumok élettartama a hívás
teljes kifejezéséig tart, így a kiírás alatt érvényesek. A függvény nem
tárolja el a referenciákat, ezért nincs dangling. A
print_tuple a tuple típusparamétereiből generál
0..N-1 indexeket, és a std::get<Is>
hívással éri el az elemeket. A std::make_tuple a
main-ben értékeket tárol, így a tuple független a
temporary-ktől.
A példa nem használ C++20-as constraint-et. Ha a
log_line-nak nem streamelhető típust adunk, a hiba a
<< kifejezésnél jelentkezik, nem az interfésznél.
C++20 concept-tel a hiba a hívás helyére hozható; ennek részletei a Concepts, requires és constrained overload részben
találhatók.
... helye. A
std::forward<Args>(args)... helyes; a
std::forward<Args...>(args)... hibás, mert a
... a template argumentumlistára kerül, nem a
függvényargumentum-mintára.sizeof...(args) a pack elemszáma; a
sizeof(args)... minden egyes elemre a sizeof
értéket adja, nem a pack méretét.std::move forwarding packnél. A
std::move(args)... mindent rvalue-vá tesz, és elveszíti a
value category-t. Helyes:
std::forward<Args>(args)....std::tuple<Args&&...> vagy
std::tuple<const Ts&...> dangling referenciát
tárolhat, ha a temporary a teljes kifejezés végén megszűnik. Tároláshoz
std::make_tuple vagy std::decay_t
szükséges.(args + ...), (args * ...) vagy
(args - ...) üres packra nem fordul; unary foldnál csak a
&&, a || és a , kap
értéket üres packra. Binary folddal, például
(0 + ... + args) alakban kezelhető.f(args..., rest...) csak akkor érvényes, ha a két pack
hossza megegyezik; különben fordítási hiba.log_line a <<-nél ad hibát; a
constraint-tel a hiba a hívás helyére hozható, és az interfész
szerződése pontosabb lesz.Magyarázd el, mi a különbség a const Ts&... és a
Ts&&... pack között a value category és az
élettartam szempontjából. Miért nem használható a
std::move(args)... a forwarding pack
továbbítására?
Írj sum és all_positive függvényt fold
expressionnel, üres pack kezeléssel, és adj
static_assert-eket. A sum üres packra 0-t, az
all_positive üres packra true-t
adjon.
Az alábbi make_refs példa dangling referenciát
eredményez. Magyarázd meg, hol szűnik meg a temporary, ki a tulajdonos,
és javítsd std::make_tuple-lel.
template <class... Args>
auto make_refs(Args&&... args) {
return std::tuple<Args&&...>(std::forward<Args>(args)...);
}
auto t = make_refs(std::string("x"));Írj apply_to_tuple függvényt
std::index_sequence-szel, amely egy callable-t és egy
tuple-t kap, és meghívja a callable-t a tuple elemeivel. Hasonlítsd
össze a std::apply-jal.
A log_line példában miért garantált a kiírás
sorrendje a , folddal, és miért nem garantált egy
+ foldnál? Adj példát && folddal
történő rövidzárlatra.
A const Ts&... pack minden argumentumot const lvalue
referenciaként köt, és a temporary élettartamát a hívás teljes
kifejezéséig meghosszabbítja. Ez olvasásra biztonságos, de a referenciák
eltárolása danglinghez vezet. A Ts&&... forwarding
reference pack minden argumentumnál megőrzi a dedukált típust és a value
category-t; a továbbításhoz
std::forward<Args>(args)... kell. A
std::move(args)... feltétel nélkül rvalue-vé tesz, így az
lvalue argumentumok is rvalue-ként továbbítódnak, és elveszik a value
category. A helyes minta:
template <class... Args>
void forward_to_sink(Args&&... args) {
sink(std::forward<Args>(args)...);
}#include <concepts>
template <std::integral... Ts>
constexpr auto sum(Ts... xs) {
return (0 + ... + xs);
}
template <std::integral... Ts>
constexpr bool all_positive(Ts... xs) {
return (true && ... && (xs > 0));
}
static_assert(sum() == 0);
static_assert(sum(1, 2, 3) == 6);
static_assert(all_positive());
static_assert(all_positive(1, 2, 3));
static_assert(!all_positive(1, -2, 3));A sum bal oldali binary fold:
((0 + x1) + x2) + ...; üres packra az iniciáló
0 marad. Az all_positive bal oldali binary
fold: ((true && (x1 > 0)) && ...); üres
packra true. A std::integral constraint
C++20-as; a static_assert-ek a fordítási időben ellenőrzik
az eredményt.
A std::string("x") temporary a make_refs
hívását tartalmazó teljes kifejezés végén megszűnik. A
make_refs visszatérő
std::tuple<std::string&&> rvalue referenciát
tárol a temporary-re. A t létrehozása után a temporary már
nem él, így a t dangling referenciát tartalmaz. A
tulajdonos a temporary volt; a t nem tulajdonos, csak nem
tulajdonló hivatkozást tárol. A temporary megszűnése invalidálja a
referenciát; a std::get<0>(t) használata nem
definiált viselkedés.
A javított változat:
#include <tuple>
#include <string>
#include <utility>
#include <iostream>
template <class... Args>
auto make_owned(Args&&... args) {
return std::make_tuple(std::forward<Args>(args)...);
}
int main() {
auto u = make_owned(std::string("x")); // biztonságos: értéket tárol
std::cout << std::get<0>(u) << '\n';
}A std::make_tuple a
std::decay_t<Args>... típusokat tárolja, így a
std::string bemásolódik vagy mozgatódik, és a
u független a temporary élettartamától.
#include <tuple>
#include <utility>
#include <type_traits>
#include <cstddef>
#include <iostream>
template <class F, class Tuple, std::size_t... Is>
decltype(auto) apply_impl(F&& f, Tuple&& t, std::index_sequence<Is...>) {
return std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(t))...);
}
template <class F, class Tuple>
decltype(auto) apply_to_tuple(F&& f, Tuple&& t) {
return apply_impl(
std::forward<F>(f), std::forward<Tuple>(t),
std::make_index_sequence<
std::tuple_size_v<std::remove_reference_t<Tuple>>>{});
}
int main() {
auto t = std::make_tuple(1, 2, 3);
auto add = [](int a, int b, int c) { return a + b + c; };
std::cout << apply_to_tuple(add, t) << '\n';
}Az apply_impl a
std::get<Is>(std::forward<Tuple>(t))... pack
expansionnal adja át a tuple elemeit a callable-nak. A
std::forward<Tuple>(t) megőrzi a tuple value
category-ját, így rvalue tuple esetén a std::get rvalue
referenciát ad. A std::apply ehhez képest
std::invoke-ot használ, így tagfüggvénypointereket és
std::reference_wrapper-t is kezel; a saját változat
közvetlen hívást végez, ezért egyszerűbb, de szűkebb.
A , operátor balról jobbra értékel, ezért a
((std::cout << args << ' '), ...) foldban a
kiírások sorrendje garantált. A + operátor operandusai
között nincs sorrendezés, ezért egy (f(args) + ...) foldban
az f hívások sorrendje nem garantált, még akkor sem, ha a
+ zárójelezése balról jobbra történik. A
&& és || rövidzárlatos; például:
template <class... Ts>
bool all_true(Ts... xs) {
return (true && ... && xs);
}Ha az első xs hamis, a későbbi elemek kiértékelése a
&& szabályai szerint elmarad. Mellékhatásos
mintánál ez megfigyelhető:
template <class... Fs>
bool any_true(Fs... fs) {
return (false || ... || fs());
}A false || ... || fs() bal oldali binary fold; az
fs() hívások balról jobbra, rövidzárlattal értékelődnek. Az
első true után a többi fs() nem hívódik
meg.
A megoldások akkor helyesek, ha a kód C++20-ban fordul, a
static_assert-ek teljesülnek, a make_refs
javított változata nem tárol referenciát temporary-re, és az
apply_to_tuple a tuple elemeit a megfelelő value
category-vel adja át. A log_line és a tuple-adapter példája
nem tartalmaz eltárolt referenciát a hívás után.
&& (true), a ||
(false) és a , (void()) érvényes,
minden más esetben kezdőértékes binary fold kell.std::index_sequence és a
std::get<Is> a tuple compile-time indexelését teszi
lehetővé; a std::apply ezt std::invoke-on
keresztül általánosítja.std::forward<Args>(args)...-tal
őrzi meg a value category-t; a const Ts&... olvasásra
biztonságos, de eltárolt referenciaként dangling lehet. Tároláshoz
std::make_tuple vagy std::decay_t kell.,, && és ||
operátorok sorrendezettek, illetve rövidzárlatosak; a + és
hasonló operátorok operandusainak kiértékelési sorrendje nem
garantált.