第15.5章 模板实参推导与 CTAD
1. 谁在写 <...>
没人会写 ::fast_io::io::println<int>(42)。那个
<int> 是从 42 推出来的,而同一套机制也在决定
vector 的元素类型(第4.3章)
和 pair 的两个类型。推导不是便利功能,它才是模板可用的原因。
要分清两个问题:
- 函数模板:实参决定模板形参,模板形参决定形参类型。
- 类模板:在 C++17 发明 CTAD(类模板实参推导)之前 没什么可推的;它把构造函数实参投影到类模板形参上。
2. 从函数调用推导
对于形如 T、T&、T const& 的每个形参,
把实参类型拆开去匹配这个模式。两条规则担起了大部分工作:
- 按值形参会退化。
T把数组和函数接成指针、去掉const、去掉引用。所以template <typename T> T max(T, T)遇到max("abc", "abd")会尝试char const *,而不是字符串。 - 引用形参不退化。
T const&保住数组:T变成char[N]。
第二条规则就是函数能知道数组长度的原因 —— 按值形参永远做不到:
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. 经验法则
- 只读参数优先用
T const&:保住数组、免拷贝、推出的正是实参类型。 T&&只和::std::forward一起用,且只用于把实参继续往下传。 一个被拷贝的转发引用,就是在等某个只能移动的类型来揭发它。- 绝不要推导只有调用者才知道的类型 ——
to<T>显式取T,这是对的。 - 隐式指引会错或有歧义时,就写一条推导指引;它只有两行,并且把意图写在了明面上。
- CTAD 并不减少实例化。
vector{i, j, k}与vector{a, b, c}是同一类型,但vector{1, 2.5}不是 —— 推导只能选一个元素类型, 混合的调用是病式的。