C++ Template: 返回值推导 Return Type
整理 C++ 模板函数返回值推导、decltype、std::decay 和 std::common_type 的基本用法。
如果 返回值类型 取决于 模版参数, 显然决定模版函数值返回值的最佳方式是由编译器自动进行推导
C++ 14
从 cpp14 开始, auto 关键字允许使用在函数的返回值上, 这使得自动推导返回值变得容易
template<typename T1, typename T2>
auto max (T1 a, T2 b)
{
return b < a ? a : b;
}但是也需要注意, 使用 auto 作为变量初始化或者函数的返回类型时, 都会发生 类型衰减 decay, 这意味着某些类型的特殊性质会被移除, 例如 引用, const/volatile 修饰符等
int i = 42;
int const& ir = i; // ir 是 i 的 const 引用
auto a = ir; // a 是一个新的对象,类型为 intC++ 11
但是在 cpp14 之前, 我们不能这么做, 必须使用 -> 来指定返回值
template<typename T1, typename T2>
auto max(T1 a, T2 b) -> decltype(true ? a : b) {
return b < a ? a : b;
}在这里, decltype(true ? a : b) 中条件始终是 true, 看似三目运算符总是选择 a, 但在编译期, 三目运算符的类型推导规则依然会同时考虑 a 和 b 的类型
在 a ? x : y 表达式中, C++ 会根据 x 和 y 的类型应用一系列规则推导出 结果类型, 而不完全取决于条件的值
- 如果
x和y的类型相同
结果的类型也是这个相同的类型 - 如果
x和y的类型不同
C++ 会尝试找到一个 公共类型 common type 通常是可以隐式转换为彼此的最小公共类型
所以, 在 decltype(true ? a : b) 中, a 和 b 的类型都会被 decltype 用于推导, decltype 在编译期推导类型时会考虑所有可能的分支, 即使条件是 true, 它仍然会分析 a 和 b 的类型来找到 公共类型 以确定最终的返回类型, 注意这发生在编译时, 而运行时逻辑依然由函数体决定, 即运行期仍然根据 b < a 来决定返回值, 而非直接返回 a
一个实际的例子:
a是int,b是double- 三目运算符
true?a:b的类型推导规则会选择double作为结果类型 (因为int可以隐式转换为double)
衰减
但是仍然有一些问题, 因为 decltype 是完整的类型推导, 它会保留 const 和 引用 等修饰符, 有时候这并不是我们期望在返回值中看到的, 所以我们需要主动 衰减 它给出的结果
这也可以看出
decltype和auto两种 自动类型推导 的不同之处,auto总是执行 衰减
#include <type_traits>
template<typename T1, typename T2>
auto max (T1 a, T2 b) -> typename std::decay<decltype(true ? a:b)>::type
{
return b < a ? a : b;
}这里使用了 type trait std::decay<>, 它会以成员 type 返回衰减后的 结果类型, 因为成员 type 是一个 类型, 所以必须使用关键字 typename 来修饰表达式才能访问它
使用公共类型作为返回值
自从 cpp11 起, cpp 标准库提供了一种办法来找到类型之间的 公共类型
std::common_type<> 接收两个或更多 类型 作为模版参数, 并返回一个 结构体, 其中的 type 成员将是输入类型之间的公共类型, 同样, 由于是 类型, 所以需要使用 typename 访问
//since C++11
typename std::common_type<T1,T2>::type自从 cpp14 开始, 可以通过在末尾添加 _t 来简化这个 traits 的使用, 这可以免于使用 typename 和 ::type
// C++ 14
std::common_type_t<T1,T2>这使得将其用于返回值变得容易
#include <type_traits>
template<typename T1, typename T2>
std::common_type_t<T1,T2>
max (T1 a, T2 b)
{
return b < a ? a : b;
}std::common_type_t 会对类型进行 衰减, 所以不会返回引用等修饰
模版参数默认值
如果我们想在推导返回值的同时提供自定义返回值类型的功能, 我们可以为返回值单独定义一个模版参数, 并设置它的 默认值
#include <type_traits>
template
<
typename RT = std::common_type_t<T1, T2>
typename T1,
typename T2
>
RT max (T1 a, T2 b)
{
return b < a ? a : b;
}我们将 RT 放在模版参数的第一个, 以便在调用时无需指定 T1 和 T2 的类型值
如此一来, 用户可以在需要时显式指定返回类型, 从而覆盖默认值, 但是如果函数设计中不存在 “自然” 的默认返回类型, 提供一个默认值可能会导致误用或意外的行为, 所以最好还是用上面的方式让编译器自动推导返回值类型