第15.5章 模板实参推导与 CTAD

English

1. 谁在写 <...>

没人会写 ::fast_io::io::println<int>(42)。那个 <int> 是从 42 推出来的,而同一套机制也在决定 vector 的元素类型(第4.3章) 和 pair 的两个类型。推导不是便利功能,它才是模板可用的原因。

要分清两个问题:

2. 从函数调用推导

对于形如 T、T&、T const& 的每个形参, 把实参类型拆开去匹配这个模式。两条规则担起了大部分工作:

第二条规则就是函数能知道数组长度的原因 —— 按值形参永远做不到:


template <typename T, ::std::size_t N>
[[nodiscard]] inline constexpr ::std::size_t array_size(T const (&)[N]) noexcept
{
	return N;
}

3. 转发引用:一个形参,两种含义

T 需要推导时的 T&& 不是右值引用,而是 转发引用。传入左值时 T 推成 U&, 形参折叠成 U&;传入右值时 T 是 U, 形参是 U&&。干活的是引用折叠规则 (& && 折成 &)。

于是 ::std::is_lvalue_reference_v<T> 就成了“我拿到的是一个名字还是一个 临时量”的编译期答案,这正是 ::std::forward 需要的,也正是 第15.11章里 ::fast_io::parameter<T&> 所依赖的:


template <typename T>
[[nodiscard]] inline constexpr ::std::string_view category(T &&) noexcept
{
	if constexpr (::std::is_lvalue_reference_v<T>)
	{
		return "lvalue";
	}
	else
	{
		return "rvalue";
	}
}

小心两个坑:只有 T 由这一次函数调用推导时,T&& 才是转发引用。在 template <typename T> void f(::std::vector<T> &&) 里,形参是普通的右值引用,从 vector<T>&& 推 T —— 完全是另一套规则。

4. 推导无能为力的场合

推导只看实参类型。如果形参类型里没有可推导位置上的模板形参,就什么都恢复不出来:


template <typename T>
[[nodiscard]] inline T from_string(::std::string_view s) throws;   // T is not deducible

// callers must say it: from_string<int>("42")

fast_io 的 ::fast_io::to<T>(...) 正是这个形状:目标类型必须显式给出, 因为输入文本不可能蕴含它。这就是为什么 第2.13章写 ::fast_io::to<::std::uint_least64_t>("12"),而从不写 ::fast_io::to("12")。

另一条出路是默认模板实参,它把“显式”变成可选而不是必需:


template <typename T = ::std::string_view>
[[nodiscard]] inline T parse(::std::string_view s) throws;

5. CTAD 与推导指引

对类模板,编译器会看构造函数实参,也会看作用域里的每一条推导指引。 隐式指引由构造函数生成;显式指引是一个没有函数体的函数声明,返回类型写明那个特化:


template <typename T>
struct pair_like
{
	T first{}, second{};

	pair_like(T a, T b) noexcept : first{a}, second{b} {}
};

template <typename A, typename B>
pair_like(A, B) -> pair_like<::std::common_type_t<A, B>>;

现在 pair_like p{1, 2.5} 推出 pair_like<double>。 构造函数生成的隐式指引会失败 —— 它要求从两个不同类型得到同一个 T;显式指引用 ::std::common_type_t 解决冲突,这也正是意图: “两者中更宽的那个类型”。

有一个细节在这个编译器上看得见:::std::float64_t 并不要求就是 double,这里也确实不是(大小相同、类型不同)。所以 decltype(p) 是 pair_like<double>,不是 pair_like<::std::float64_t>。这不是缺陷:指引写的是 common_type_t,而标准对 int 与 double 的答案就是 double。

fast_io 的容器自带指引,所以分配器从不用写 (第15.9章给出真实代码): vector{1, 2, 3} 推出 vector<int> 加默认分配器, vector{::std::from_range, r} 从范围推出元素类型。

6. 完整程序


#include <fast_io.h>
#include <cstddef>
#include <string_view>
#include <type_traits>

template <typename T>
[[nodiscard]] inline constexpr ::std::string_view category(T &&) noexcept
{
	if constexpr (::std::is_lvalue_reference_v<T>)
	{
		return "lvalue";
	}
	else
	{
		return "rvalue";
	}
}

template <typename T, ::std::size_t N>
[[nodiscard]] inline constexpr ::std::size_t array_size(T const (&)[N]) noexcept
{
	return N;
}

template <typename T>
struct pair_like
{
	T first{}, second{};

	pair_like(T a, T b) noexcept : first{a}, second{b} {}
};

template <typename A, typename B>
pair_like(A, B) -> pair_like<::std::common_type_t<A, B>>;

int main()
{
	try
	{
		int x{1};
		::fast_io::io::println(category(x), " ", category(2));

		int const arr[5]{};
		::fast_io::io::println("array_size=", array_size(arr));

		pair_like p{1, 2.5};
		static_assert(::std::same_as<decltype(p), pair_like<double>>);
		::fast_io::io::println("first=", p.first, " second=", p.second);
	}
	catch throws (::std::error e)
	{
		::fast_io::io::perrln(e);
	}
}

输出:


lvalue rvalue
array_size=5
first=1 second=2.5

7. 经验法则