Ch15.6: Variadic Templates and Fold Expressions
1. Zero or More of the Same Thing
::fast_io::io::println(1, "two", 3.0, 'x') takes four arguments of four
types and is one function. That is a parameter pack:
template <typename... Ts>
inline void println(Ts const &...args) throws;
typename... Tsdeclares a type pack: zero or more types.Ts const &...argsdeclares a function parameter pack: zero or more parameters, one perTs.sizeof...(Ts)is the count, as a compile-time constant.args...is a pack expansion: it repeats the pattern before the ellipsis once per element.
Before C++17 a variadic template had to be consumed by recursion — one instantiation per element, and a base case to stop. Fold expressions removed the recursion, and with it most of the instantiations.
2. Fold Expressions
A fold applies a binary operator across the whole pack. There are four shapes:
| Shape | Means |
|---|---|
(args + ...) |
unary right: a + (b + (c + d)) |
(... + args) |
unary left: ((a + b) + c) + d |
(args + ... + 0) |
binary right, with an init: empty pack gives 0 |
(0 + ... + args) |
binary left, with an init |
The init matters: a unary fold over an empty pack is only legal for &&,
|| and ,. sum() with a unary
(args + ...) does not compile; (args + ... + 0) returns
0, which is also the type-stable answer (an int, not a
compiler error):
template <typename... Ts>
[[nodiscard]] inline auto sum(Ts... args) noexcept
{
return (args + ... + 0);
}
A fold also works over an operator you write yourself, and over a concept. The
&& fold below is how you ask “are all of these types
integral” without a recursive trait:
template <typename... Ts>
concept all_integral = (::std::integral<Ts> && ...);
3. Recursion That Survives: a Base Case with if constexpr
Some packs cannot be folded, because the operation is not associative or because it
has to stop early. max_of is the standard example: the answer depends on
comparing every element with the maximum of the rest.
template <typename first, typename... rest>
[[nodiscard]] inline constexpr auto max_of(first head, rest... tail) noexcept
{
if constexpr (sizeof...(tail) == 0)
{
return head;
}
else
{
auto const rest_max{max_of(tail...)};
return head < rest_max ? rest_max : head;
}
}
if constexpr is what makes the recursion terminate: the
else branch is discarded when the pack is empty, so no call to
max_of() with zero arguments is ever instantiated. An ordinary
if would not compile at all — both branches must be valid
(Ch15.8).
Note the noexcept: comparing numbers cannot fail, and the promise holds
for every instantiation. If one element's operator< could throw, the
whole function would have to be throws.
4. Perfect Forwarding: Do Not Lose the Value Category
A variadic function that passes arguments on must preserve two things per
argument: its type and its value category. Taking them by
Ts const& loses the second one — everything arrives as an
lvalue and is copied. The fix is Args&& plus
::std::forward:
template <typename T, typename... Args>
[[nodiscard]] inline T *create(Args &&...args) throws
{
auto const res{::fast_io::native_global_allocator::allocate_aligned_at_least(alignof(T), sizeof(T))};
return ::new (static_cast<void *>(res.ptr)) T(::std::forward<Args>(args)...);
}
Args&&is a forwarding reference per element (Ch15.5): an lvalue deducesU&, an rvalue deducesU.::std::forward<Args>(args)restores the value category from the deduced type, so an lvalue is passed as an lvalue and an rvalue as an rvalue.- The
...afterargsinsidestd::forward<Args>(args)...expands the whole pattern, so each argument keeps its own type.
That is not a theoretical difference. A type that counts its own copies and moves proves it in one line each — lvalue in: one copy, no move; rvalue in: no copy, one move:
struct probe
{
::std::size_t copies{};
::std::size_t moves{};
probe() noexcept = default;
probe(probe const &other) noexcept : copies{other.copies + 1}, moves{other.moves} {}
probe(probe &&other) noexcept : copies{other.copies}, moves{other.moves + 1} {}
};
Replace ::std::forward with a plain args... and the rvalue
case becomes a second copy — the bug that shows up as “why is my
move-only type not movable”.
5. Expanding the Pack onto a Call
The commonest use of a pack is not arithmetic, it is forwarding into something else:
template <typename... Ts>
inline void print_each(Ts const &...args) throws
{
(::fast_io::io::println(args), ...);
}
The fold is over the comma operator with a pattern that mentions args,
so it expands to one println per element, in order. Comma is one of the
three operators that fold legally over an empty pack, so
print_each() compiles and does nothing — which is what you want.
println here is the throws one, so
print_each is throws too: an error contract is contagious
up the call chain, and pretending otherwise with noexcept would call
::std::terminate on a broken pipe
(Ch3.16).
6. The Whole Program
#include <fast_io.h>
#include <cstddef>
#include <string_view>
#include <type_traits>
#include <utility>
using namespace ::std::string_view_literals;
template <typename... Ts>
[[nodiscard]] inline auto sum(Ts... args) noexcept
{
return (args + ... + 0);
}
template <typename... Ts>
[[nodiscard]] inline constexpr ::std::size_t count_of(Ts...) noexcept
{
return sizeof...(Ts);
}
template <typename first, typename... rest>
[[nodiscard]] inline constexpr auto max_of(first head, rest... tail) noexcept
{
if constexpr (sizeof...(tail) == 0)
{
return head;
}
else
{
auto const rest_max{max_of(tail...)};
return head < rest_max ? rest_max : head;
}
}
template <typename... Ts>
concept all_integral = (::std::integral<Ts> && ...);
template <typename... Ts>
inline void print_each(Ts const &...args) throws
{
(::fast_io::io::println(args), ...);
}
struct probe
{
::std::size_t copies{};
::std::size_t moves{};
probe() noexcept = default;
probe(probe const &other) noexcept : copies{other.copies + 1}, moves{other.moves} {}
probe(probe &&other) noexcept : copies{other.copies}, moves{other.moves + 1} {}
};
template <typename T, typename... Args>
[[nodiscard]] inline T *create(Args &&...args) throws
{
auto const res{::fast_io::native_global_allocator::allocate_aligned_at_least(alignof(T), sizeof(T))};
return ::new (static_cast<void *>(res.ptr)) T(::std::forward<Args>(args)...);
}
int main()
{
try
{
::fast_io::io::println(sum(1, 2, 3, 4));
::fast_io::io::println(count_of(1, 2.5, "three"sv));
::fast_io::io::println(max_of(1, 4, 2));
::fast_io::io::println("all_integral of int,char=", all_integral<int, char>);
probe p{};
probe *a{create<probe>(p)};
probe *b{create<probe>(probe{})};
::fast_io::io::println("lvalue: copies=", a->copies, " moves=", a->moves);
::fast_io::io::println("rvalue: copies=", b->copies, " moves=", b->moves);
print_each(1, "two"sv, 3);
}
catch throws (::std::error e)
{
::fast_io::io::perrln(e);
}
}
Output:
10
3
4
all_integral of int,char=1
lvalue: copies=1 moves=0
rvalue: copies=0 moves=1
1
two
3
7. Rules of Thumb
- Reach for a fold before a recursive variadic template. Each recursion level is another instantiation.
- Always write the init (
(args + ... + 0)) when the empty pack has to mean something. - Forward, never copy, when passing a pack on:
Args&&...and::std::forward<Args>(args).... - A variadic function that only wraps another call should be
inlineand one line long; everything longer belongs in a non-template helper (Ch15.9). - Do not fold over
+or*a pack ofchars or small integers without thinking about promotion: the fold accumulates inint.