类型推断 miniWiki

模板类型推断

形参与实参

考虑如下函数模板调用语句

template <typename T>
void func(ParaType parameter) {
  /* ... */
}
ArguType argument;  // argument 的类型为 ArguType
func(argument);     // 根据 ArguType 推断 ParaType

其中

  • T 为留待编译器推断的模板形参 (template parameter)
  • parameter函数形参 (function parameter)ParaType 是它的类型,后者
    • 既可以是 T,也可以是 T*T&T&&复合类型
    • 还可以是 std::vector<T>std::set<T>容器类型
  • argument函数实参 (function argument)ArguType 是它的类型,前者
    • 既可以是右值表达式 (rvalue expression),例如 1 + 1
    • 也可以是左值表达式 (lvalue expression),例如由变量名构成的表达式,变量本身可以是任何类型。

编译器通过比较 ParaTypeArguType 来推断 T

  1. 忽略 ArguType 的 RCV 属性,其中 R、C、V 分别表示:引用属性顶层 const 属性顶层 volatile 属性
  2. 将上一步所得类型与 ParaType 比较,以所需修饰符最少的类型作为 T

为方便讨论,这里先定义一组变量,变量名的含义在注释中用大写字母给出:

      int            i =  0;  //                    Int
const int           ci =  i;  //              Const Int
      int &         ri =  i;  //       Ref to       Int
const int &        rci =  i;  //       Ref to Const Int
      int &&       rri =  0;  // Right Ref to       Int
      int *         pi = &i;  //       Ptr to       Int
const int *        pci = &i;  //       Ptr to Const Int
      int * const  cpi = &i;  // Const Ptr to       Int
const int * const cpci = &i;  // Const Ptr to Const Int

ParaType 不是指针或引用

这条情况对应于传值调用 (pass-by-value): 函数内部所使用的对象是 argument独立副本,因此 argument顶层 const 属性顶层 volatile 属性 对这个独立副本没有影响。

ParaType = T

推断过程及结果如下:

argument ArguType 忽略右值引用 忽略顶层 CV T
0 int     int
i int     int
ci int const   int int
ri int & int   int
rci int const & int const int int
rri int && int   int
pi int *     int *
pci int const *     int const *
cpi int * const   int * int *
cpci int const * const   int const * int const *

ParaType = const T

(这里的 const T 也可以写成 T const

推断过程及 T 的推断结果与上一种情形相同,只是 ParaType 多出一个顶层 const 属性

ParaType 为指针

此时 ArguType 必须是指针(或对指针的引用)。

ParaType = T *

此时,底层 const 属性(被指对象的 const 属性)会被推断为 T 的一部分:

argument ArguType 忽略 RCV T * T
pi int *   int * int
pci int const *   int const * int const
cpi int * const int * int * int
cpci int const * const int const * int const * int const

ParaType = T * const

推断过程及 T 的推断结果与 ParaType = T * 的情形相同,只是 ParaType 多出一个顶层 const 属性

ParaType = T const *

(这里的 T const * 也可以写成 const T *

此时,ArguType底层 const 属性已体现在 ParaType 中,因此 T 的推断结果不含这个底层 const 属性

argument ArguType 忽略 RCV T const * T
pi int *   int const * int
pci int const *     int
cpi int * const int * int const * int
cpci int const * const int const * int const * int

ParaType = T const * const

(这里的 T const * const 有可以写成 const T * const

推断过程及 T 的推断结果与 ParaType = T const *ParaType = const T * 的的情形相同,只是 ParaType 多出一个顶层 const 属性

ParaType 为引用

此时 ArguType 可以是任意类型。

ParaType = T &

此时 argument 必须是左值表达式。如果它含有顶层或底层 const 属性,则会被推断为 T 的一部分:

argument ArguType ParaType T
i int int & int
ci int const int const & int const
pi int * int * & int *
pci int const * int const * & int const *
cpi int * const int * const & int * const
cpci int const * const int const * const & int const * const

最后两行是推断结果含顶层 const 属性的例子:

  • argument 是带有顶层 const 属性的指针,即这个指针本身const
  • parameter 是对 const 指针的引用,这个 const 成为了底层 const 属性
  • 因此 T 的推断结果中含有 argument顶层 const 属性

ParaType = T const &

(这里的 T const & 也可以写成 const T &

此时 argument 可以是任意表达式。如果它含有底层 const 属性,则会被推断为 T 的一部分,而顶层 const 属性则会被忽略:

argument ArguType T const & T
0 int int const & int
i int int const & int
ci int const int const & int
pi int * int * const & int *
pci int const * int const * const & int const *
cpi int * const int * const & int *

ParaType = T &&

Scott Meyers 所著的《Effective Modern C++》中,形如 T &&T 需要被推断的引用(例如 ParaType = T &&auto &&)被称为万能引用 (universal reference)

万能引用 T && 中的待定类型 T 按以下规则推断:

  1. argument左值表达式,则 T左值引用,否则 T 不含引用。
  2. 若上述推断结果含多重引用,则按引用折叠规则处理。

引用折叠 (reference collapsing) 规则:

假设 X 是不含引用的类型,则

  • X && && 折叠为 X &&,即纯右值引用。
  • 其他情形(X & &&X && &X & &)均折叠为 X &,即纯左值引用。

根据以上规则,argument 可以是任意类型:

argument ArguType 表达式类型 T T &&
0 int R int int &&
std::move(i) int && R int int &&
i int L int & int &
ci int const L int const & int const &
ri int & L int & int &
rci int const & L int const & int const &
rri int && L int & int &
pi int * L int * & int * &
pci int const * L int const * & int const * &
cpi int * const L int * const & int * const &
cpci int const * const L int const * const & int const * const &

万能引用几乎总是与 std::forward<T>() 配合使用,以达到完美转发 (perfect forward) 函数实参的目的。 这里的完美是指:避免不必要的拷贝或移动,并且保留函数实参的所有类型信息(包括 RCV 属性)。 它的实现需要用到模板元编程技术。 典型应用场景为向构造函数完美转发实参

#include <utility>
#include <vector>
template <class T>
std::vector<T> build(T&& x) {  // T&& 是一个『万能引用』
  auto v =  std::vector<T>(std::forward<T>(x));
  // decorate v
  return v;
}

数组或函数

如果 argument数组函数(或对它们的引用),则 ParaType 必须含引用,否则 argument退化 (decay) 为指针:

template <typename T, typename U>
void f(T, U&) { /* ... */ }
// argument 为数组:
const char book[] = "C++ Primer";  // book 的类型为 const char[11]
f(book, book);  // 推断结果为 void f(const char*, const char(&)[11])
// argument 为函数:
int g(double);
f(g, g);  // 推断结果为 void f(int(*)(double), int(&)(double))

其他类型推断

auto 类型推断

一般情况:与模板类型推断相同

自 C++11 起,可以将 auto 用作变量类型。如果使用得当,可以大大简化代码。 一般情况下,auto 类型推断模板类型推断的规则相同。 在这些场合,auto 实际上就是模板类型形参 T,而其他元素有如下对应关系:

auto 语句 ParaType parameter argument ArguType
auto i = 0; auto i 0 int
const auto & j = 1; const auto & j 1 int
auto&& k = 2; auto && k 2 int

自 C++14 起,还可以将 auto 用作函数返回类型lambda 形参类型

auto func(int* p) {
  return *p;  // *p 的类型是 int &,因此 auto 被推断为 int
}
auto is_positive = [](const auto& x) { return x > 0; };
is_positive(3.14);  // auto 被推断为 double
is_positive(-256);  // auto 被推断为 int

唯一例外:列表初始化

对于 int,有以下四种几乎等价的初始化方式,得到的都是 int 变量:

int a = 1;
int b(2);
int c{3};
int d = {4};

但如果换作 auto

auto a = 1;
auto b(2);
auto c{3};
auto d = {4};

最后一种方式得到的是只含一个元素的 std::initializer_list<int>。 ⚠️ 这是唯一一处 auto 类型推断不同于模板类型推断的地方。

二者的区别在下面的例子中体现得更为明显:

#include <initializer_list>  // 不可省略
auto x = {1, 2, 3};  // x 为含有 3 个元素的 std::initializer_list<int>
//  等价的函数模板定义:
template <typename T>
int f(T parameter) {
  return sizeof(parameter);
}
//  正确的函数模板定义:
template <typename T>
int g(std::initializer_list<T> parameter) {
  return sizeof(parameter);
}
// 对比:
int main() {
  f(x);          // T 被推断为 std::initializer_list<int>
  f({1, 2, 3});  // 类型推断失败
  g(x);          // T 被推断为 std::initializer_list<int>
  g({1, 2, 3});  // T 被推断为 int
}

decltype 类型推断

decltype 是一种修饰符 (specifier),它作用在表达式 expr 上得到其类型 ExprType

  • 一般情况下,ExprTypeexpr 的类型(含 RCV 属性)。
  • 如果 expr 是一个左值表达式但不是变量名,则 ExprType 还需附加一个左值引用
  • 如果 expr 是一个只含变量名左值表达式,则 ExprType 不附加额外的左值引用
#include <type_traits>  // std::is_same
using std::is_same_v;   // C++17

int i = 0;
static_assert(is_same_v<decltype( i ), int >);  // 只含变量名
static_assert(is_same_v<decltype((i)), int&>);  // 左值表达式

int&& rri = 0;
static_assert(is_same_v<decltype( rri ), int&&>);  // 只含变量名
static_assert(is_same_v<decltype((rri)), int& >);  // 左值表达式 + 引用折叠

void f(const int& x) {
  static_assert(is_same_v<decltype(x), const int&>);  // 保留 RCV 属性
}
auto* pf = f;
auto& rf = f;
static_assert(is_same_v<decltype( f), void   (const int&)>);  // 函数
static_assert(is_same_v<decltype(pf), void(*)(const int&)>);  // 函数指针
static_assert(is_same_v<decltype(rf), void(&)(const int&)>);  // 对函数的引用

int a[] = {1, 2, 3};
auto* pa = a;
auto& ra = a;
static_assert(is_same_v<decltype( a), int   [3]>);  // 数组
static_assert(is_same_v<decltype(ra), int(&)[3]>);  // 对数组的引用
static_assert(is_same_v<decltype(pa), int *    >);  // 指向数组首元的指针
static_assert(is_same_v<decltype(*a), int &    >);  // 对数组首元的引用

函数返回类型推断

考虑如下函数

ReturnType func(ParaType parameter) {
  return expr;  // expr 的类型为 ExprType
}

后置返回类型 (C++11)

如果希望以 ExprType 作为 ReturnType,则只需要以 auto 作为 ReturnType,并在函数形参列表函数体之间插入 -> decltype(expr)

auto func(ParaType parameter) -> decltype(expr) {
  return expr;  // expr 的类型为 ExprType
}

这里的 auto 只是一个占位符,实际推断工作是由 decltype 来完成的,因此需遵循 decltype 类型推断规则。 按此规则:如果 expr 是一个左值表达式但不是变量名,则 decltype 会为 ExprType 附加一个左值引用,即以 ReturnType = ExprType &

如果希望去掉返回值的引用属性(无论是 ExprType 本身所含有的,还是 decltype 附加上的),则需借助 std::remove_reference

#include <type_traits>  // std::remove_reference
auto func(ParaType parameter)
    -> typename std::remove_reference<decltype(expr)>::type {
  return expr;  // expr 的类型为 ExprType
}

前置返回类型 (C++14)

C++14 允许以 auto 作为返回类型:

auto func(ParaType parameter) {
  return expr;  // expr 的类型为 ExprType
}

这里的 auto 承担了类型推断任务,因此需遵循 auto 类型推断规则。 按此规则:expr 的 RCV 属性会丢失。

也可以将 decltype(auto) 用作 ReturnType,此时需遵循 decltype 类型推断规则:

decltype(auto) func(ParaType parameter) {
  return expr;
}

⚠️ 以 decltype(auto) 作为 ReturnType 需避免返回对局部变量的引用

decltype(auto) func() {
  auto v = std::vector<int>{1, 2, 3};
  return v[0];  // 危险行为:返回对 局部变量 的引用
}