第15.6章 变参模板、折叠表达式、完美转发

English

1. 同一个东西的零个或多个

::fast_io::io::println(1, "two", 3.0, 'x') 接四个实参、四种类型,却是同一个 函数。这是一个参数包:


template <typename... Ts>
inline void println(Ts const &...args) throws;

C++17 之前,变参模板只能靠递归消费 —— 每个元素一层实例化,外加一个终止的基例。 折叠表达式消灭了递归,也消灭了绝大部分实例化。

2. 折叠表达式

折叠把二元运算符施加到整个包上。四种形状:

形状 含义
(args + ...) 一元右折叠:a + (b + (c + d))
(... + args) 一元左折叠:((a + b) + c) + d
(args + ... + 0) 二元右折叠,带初值:空包给出 0
(0 + ... + args) 二元左折叠,带初值

初值很要紧:对空包的一元折叠只有 &&、||、, 合法。写成 (args + ...) 的 sum() 编译不过; (args + ... + 0) 返回 0,而且类型也稳定(是 int,不是编译错误):


template <typename... Ts>
[[nodiscard]] inline auto sum(Ts... args) noexcept
{
	return (args + ... + 0);
}

折叠也适用于你自己写的运算符,以及概念。下面的 && 折叠就是 “这些类型是不是全为整型”的写法,不需要递归特征:


template <typename... Ts>
concept all_integral = (::std::integral<Ts> && ...);

3. 活下来的递归:用 if constexpr 写基例

有些包折叠不了,因为运算不满足结合律,或者需要提前停下。max_of 是标准例子:答案取决于每个元素与“其余部分最大值”的比较。


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:包为空时 else 分支被丢弃, 因此永远不会实例化一个零实参的 max_of()。普通 if 根本编译不过 —— 两个分支都必须合法 (第15.8章)。

注意 noexcept:比较数字不会失败,而且这个承诺对所有实例化都成立。 若某个元素的 operator< 可能抛,整个函数就必须是 throws。

4. 完美转发:不要丢掉值类别

一个把实参继续往下传的变参函数,必须为每个实参保住两样东西:类型与值类别。 用 Ts const& 接会丢掉后者 —— 一切都变成左值并产生拷贝。 解法是 Args&& 加 ::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)...);
}

这不是理论差别。一个统计自己拷贝与移动次数的类型各用一行就能证明 —— 传左值:一次拷贝、零次移动;传右值:零次拷贝、一次移动:


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} {}
};

把 ::std::forward 换成朴素的 args...,右值那一格就变成第二次拷贝 —— 这就是“为什么我那个只能移动的类型移不动”这个 bug。

5. 把包展开到一次调用上

包最常见的用途不是算术,而是转交给别的东西:


template <typename... Ts>
inline void print_each(Ts const &...args) throws
{
	(::fast_io::io::println(args), ...);
}

这是对逗号运算符的折叠,模式里出现了 args,于是展开成每个元素一次 println,顺序保持。逗号是能对空包合法折叠的三个运算符之一,所以 print_each() 能编译并且什么都不做 —— 这正是你要的。

这里的 println 是 throws 那个,所以 print_each 也是 throws:错误契约会沿调用链传染, 用 noexcept 假装不是,只会在管道断掉时调用 ::std::terminate(第3.16章)。

6. 完整程序


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

输出:


10
3
4
all_integral of int,char=1
lvalue: copies=1 moves=0
rvalue: copies=0 moves=1
1
two
3

7. 经验法则