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;

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)...);
}

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