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 是一个新的对象,类型为 int

C++ 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, 但在编译期, 三目运算符的类型推导规则依然会同时考虑 ab 的类型

a ? x : y 表达式中, C++ 会根据 xy 的类型应用一系列规则推导出 结果类型, 而不完全取决于条件的值

  • 如果 xy 的类型相同
    结果的类型也是这个相同的类型
  • 如果 xy 的类型不同
    C++ 会尝试找到一个 公共类型 common type 通常是可以隐式转换为彼此的最小公共类型

所以, 在 decltype(true ? a : b) 中, ab 的类型都会被 decltype 用于推导, decltype 在编译期推导类型时会考虑所有可能的分支, 即使条件是 true, 它仍然会分析 ab 的类型来找到 公共类型 以确定最终的返回类型, 注意这发生在编译时, 而运行时逻辑依然由函数体决定, 即运行期仍然根据 b < a 来决定返回值, 而非直接返回 a

一个实际的例子:

  1. aint, bdouble
  2. 三目运算符 true ? a : b 的类型推导规则会选择 double 作为结果类型 (因为 int 可以隐式转换为 double)

衰减

但是仍然有一些问题, 因为 decltype 是完整的类型推导, 它会保留 const引用 等修饰符, 有时候这并不是我们期望在返回值中看到的, 所以我们需要主动 衰减 它给出的结果

这也可以看出 decltypeauto 两种 自动类型推导 的不同之处, 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 放在模版参数的第一个, 以便在调用时无需指定 T1T2 的类型值

如此一来, 用户可以在需要时显式指定返回类型, 从而覆盖默认值, 但是如果函数设计中不存在 “自然” 的默认返回类型, 提供一个默认值可能会导致误用或意外的行为, 所以最好还是用上面的方式让编译器自动推导返回值类型