01 类型推断

优质
小牛编辑
132浏览
2023-12-01

01 模板类型推断机制

  • auto 推断的基础是模板类型推断机制,但部分特殊情况下,模板推断机制不适用于 auto
  • 模板的形式可以看成如下伪代码
template<typename T>
void f(ParamType x); // ParamType 即 x 的类型
  • 调用可看成
f(expr);
  • 编译期间,编译器用 expr 推断 T 和 ParamType,实际上两者通常不一致,比如
template<typename T>
void f(const T& x);

int x; // 为方便演示,只指定类型不初始化,后续同理
f(x); // T 被推断为 int,ParamType 被推断为 const int&
  • T 的类型推断与 expr 和 ParamType 相关

情形 1:ParamType 不是引用或指针

  • 丢弃 expr 的 top-level cv 限定符和引用限定符,最后得到的 expr 类型就是 T 和 ParamType 的类型
template<typename T>
void f(T x);

int a;
const int b;
const int& c;

int* p1;
const int* p2;
int* const p3;
const int* const p4;

char s1[] = "downdemo";
const char s2[] = "downdemo";

// 以下情况 T 和 ParamType 都是 int
f(a);
f(b);
f(c);
// 指针类型丢弃的是 top-level const(即指针本身的 const)
// low-level const(即所指对象的 const)会保留
f(p1); // T 和 ParamType 都是 int*
f(p2); // T 和 ParamType 都是 const int*
f(p3); // T 和 ParamType 都是 int*
f(p4); // T 和 ParamType 都是 const int*
// char 数组会退化为指针
f(s1); // T 和 ParamType 都是 char*
f(s2); // T 和 ParamType 都是 const char*

情形 2:ParamType 是引用类型

  • 如果 expr 的类型是引用,保留 cv 限定符,ParamType 一定是左值引用类型,ParamType 去掉引用符就是 T 的类型,即 T 一定不是引用类型
template<typename T>
void f(T& x);

int a;
int& b;
int&& c;
const int d;
const int& e;

int* p1;
const int* p2;
int* const p3;
const int* const p4;

char s1[] = "downdemo";
const char s2[] = "downdemo";

f(a); // ParamType 是 int&,T 是 int
f(b); // ParamType 是 int&,T 是 int
f(c); // ParamType 是int&,T是int
f(d); // ParamType 是 const int&,T 是 const int
f(e); // ParamType 是 const int&,T 是 const int
// 因为 top-level const 和 low-level const 都保留
// 对于指针只要记住 const 的情况和实参类型一样
f(p1); // ParamType 是 int* &,T 是 int*
f(p2); // ParamType 是 const int* &,T 是const int*
f(p3); // ParamType 是 int* const&,T 是 int* const
f(p4); // ParamType 是 const int* const &,T 是 const int* const
// 数组类型对于 T& 的情况比较特殊,不会退化到指针
f(s1); // ParamType 是 char(&)[9],T 是 char[9]
f(s2); // ParamType 是 const char(&)[9],T 是 const char[9]
  • 如果把 ParamType 从 T& 改为 const T&,区别只是 ParamType 一定为 top-level const,ParamType 去掉 top-level const 和引用符就是 T 的类型,即 T 一定不为 top-level const 引用类型
template<typename T>
void f(const T& x);

int a;
int& b;
int&& c;
const int d;
const int& e;

int* p1;
const int* p2;
int* const p3;
const int* const p4;

char s1[] = "downdemo";
const char s2[] = "downdemo";

// 以下情况 ParamType 都是 const int&,T 都是 int
f(a);
f(b);
f(c);
f(d);
f(e);
// 数组类型类似
f(s1); // ParamType 是 const char(&)[9],T 是 char[9]
f(s2); // ParamType 是 const char(&)[9],T 是 char[9]
// 对于指针只要记住,T的指针符后一定无const
f(p1); // ParamType 是 int* const &,T 是 int*
f(p2); // ParamType 是 const int* const &,T 是 const int*
f(p3); // ParamType 是 int* const&,T 是 int*
f(p4); // ParamType 是 const int* const &,T 是 const int*
  • 对应数组类型的模板参数类型应声明为 T(&)[N],即数组类型 T[N] 的引用
template<typename T, std::size_t N>
constexpr std::size_t f(T(&)[N]) noexcept
{
  return N;
}

const char s[] = "downdemo";
int a[f(s)]; // int a[9]

情形 3:ParamType 是指针类型

  • 与情形 2 类似,ParamType 一定是 non-const 指针(传参时忽略 top-level const)类型,去掉指针符就是 T 的类型,即 T 一定不为指针类型
template<typename T>
void f(T* x);

int a;
const int b;

int* p1;
const int* p2;
int* const p3; // 传参时与 p1 类型一致
const int* const p4; // 传参时与 p2 类型一致

char s1[] = "downdemo";
const char s2[] = "downdemo";

f(&a); // ParamType 是 int*,T 是 int
f(&b); // ParamType 是const int*,T 是 const int

f(p1); // ParamType 是 int*,T 是int
f(p2); // ParamType 是 const int*,T 是 const int
f(p3); // ParamType 是 int*,T 是 int
f(p4); // ParamType 是 const int*,T 是 const int

// 数组类型会转为指针类型
f(s1); // ParamType 是 char*,T 是 char
f(s2); // ParamType 是 const char*,T 是 const char
  • 如果 ParamType 是 const-pointer,和上面实际上是同一个模板,ParamType 多出 top-level const,T 不变
template<typename T>
void f(T* const x);

int a;
const int b;

int* p1; // 传参时与 p3 类型一致
const int* p2; // 传参时与 p4 类型一致
int* const p3;
const int* const p4;

char s1[] = "downdemo";
const char s2[] = "downdemo";

f(&a); // ParamType 是 int* const,T 是 int
f(&b); // ParamType 是 const int* const,T 是 const int

f(p1); // ParamType 是 int* const,T 是 int
f(p2); // ParamType 是 const int* const,T 是 const int
f(p3); // ParamType 是 int* const,T 是 int
f(p4); // ParamType 是 const int* const,T 是 const int

f(s1); // ParamType 是 char* const,T 是 char
f(s2); // ParamType 是 const char* const,T 是 const char
  • 如果 ParamType 是 pointer to const,则只有一种结果,T 一定是不带 const 的非指针类型
template<typename T>
void f(const T* x);

template<typename T>
void g(const T* const x);

int a;
const int b;

int* p1;
const int* p2;
int* const p3;
const int* const p4;

char s1[] = "downdemo";
const char s2[] = "downdemo";

// 以下情况ParamType 都是 const int*,T 都是 int
f(&a);
f(&b);
f(p1);
f(p2);
f(p3);
f(p4);
// 以下情况ParamType 都是 const int* const,T 都是 int
g(&a);
g(&b);
g(p1);
g(p2);
g(p3);
g(p4);
// 以下情况ParamType 都是 const char*,T 都是 char
f(s1);
f(s2);
g(s1);
g(s2);

情形 4:ParamType 是转发引用

  • 如果 expr 是左值,T 和 ParamType 都推断为左值引用。这有两点非常特殊
    • 这是 T 被推断为引用的唯一情形
    • ParamType 使用右值引用语法,却被推断为左值引用
  • 如果 expr 是右值,则 ParamType 推断为右值引用类型,去掉 && 就是 T 的类型,即 T 一定不为引用类型
template<typename T>
void f(T&& x);

int a;
const int b;
const int& c;
int&& d = 1; // d 是右值引用,也是左值,右值引用是只能绑定右值的引用而不是右值

char s1[] = "downdemo";
const char s2[] = "downdemo";

f(a); // ParamType 和 T 都是 int&
f(b); // ParamType 和 T 都是 const int&
f(c); // ParamType 和 T 都是 const int&
f(d); // ParamType 和 T 都是 const int&
f(1); // ParamType 是 int&&,T 是 int

f(s1); // ParamType 和 T 都是 char(&)[9]
f(s2); // ParamType 和 T 都是 const char(&)[9]

特殊情形:expr 是函数名

template<typename T> void f1(T x);
template<typename T> void f2(T& x);
template<typename T> void f3(T&& x);

void g(int);

f1(g); // T 和 ParamType 都是 void(*)(int)
f2(g); // ParamType 是 void(&)(int),T 是 void()(int)
f3(g); // T 和 ParamType 都是 void(&)(int)

02 auto 类型推断机制

  • auto 类型推断几乎和模板类型推断一致
  • 调用模板时,编译器根据 expr 推断 T 和 ParamType 的类型。当变量用 auto 声明时,auto 就扮演了模板中的T的角色,变量的类型修饰符则扮演 ParamType 的角色
  • 为了推断变量类型,编译器表现得好比每个声明对应一个模板,模板的调用就相当于对应的初始化表达式
auto x = 1;
const auto cx = x;
const auto& rx = x;

template<typename T> // 用来推断 x 类型的概念上假想的模板
void func_for_x(T x);

func_for_x(1); // 假想的调用: param 的推断类型就是 x 的类型

template<typename T> // 用来推断 cx 类型的概念上假想的模板
void func_for_cx(const T x);

func_for_cx(x); // 假想的调用: param 的推断类型就是 cx 的类型

template<typename T> // 用来推断 rx 类型的概念上假想的模板
void func_for_rx(const T& x);

func_for_rx(x); // 假想的调用: param 的推断类型就是 rx 的类型
  • auto 的推断适用模板推断机制的三种情形:T&、T&& 和 T
auto x = 1; // int x
const auto cx = x; // const int cx
const auto& rx = x; // const int& rx
auto&& uref1 = x; // int& uref1
auto&& uref2 = cx; // const int& uref2
auto&& uref3 = 1; // int&& uref3
  • auto 对数组和指针的推断也和模板一致
const char name[] = "downdemo"; // 数组类型是 const char[9]
auto arr1 = name; // const char* arr1
auto& arr2 = name; // const char (&arr2)[9]

void g(int, double); // 函数类型是 void(int, double)
auto f1 = g; // void (*f1)(int, double)
auto& f2 = g; // void (&f2)(int, double)
  • auto 推断唯一不同于模板实参推断的情形是 C++11 的初始化列表。下面是同样的赋值功能
// C++98
int x1 = 1;
int x2(1);
// C++11
int x3 = { 1 };
int x4{ 1 };
  • 但换成 auto 声明,这些赋值的意义就不一样了
auto x1 = 1; // int x1
auto x2(1); // int x2
auto x3 = { 1 }; // std::initializer_list<int> x3
auto x4{ 1 }; // C++11 为 std::initializer_list<int> x4,C++14 为 int x4
  • 如果初始化列表中元素类型不同,则无法推断
auto x5 = { 1, 2, 3.0 }; // 错误:不能为 std::initializer_list<T> 推断 T
auto x1 = { 1, 2 }; // C++14 中必须用 =,否则报错
auto x2 { 1 }; // 允许单元素的直接初始化,不会将其视为 initializer_list
  • 模板不支持模板参数为 T 而 expr 为初始化列表的推断,不会将其假设为 std::initializer_list,这就是 auto 推断和模板推断唯一的不同之处
auto x = { 1, 2, 3 }; // x 类型是 std::initializer_list<int>

template<typename T> // 等价于 x 声明的模板
void f(T x);

f({ 1, 2, 3 }); // 错误:不能推断 T 的类型
template<typename T>
void f(std::initializer_list<T> initList);

f({ 11, 23, 9 }); // T 被推断为 int,initList 类型是 std::initializer_list<int>
  • 对于 C++11,auto 的介绍就到此为止了

C++14 的 auto

  • C++14中,auto 可以作为函数返回类型,并且 lambda 可以将参数声明为 auto,这种 lambda 称为泛型 lambda
auto f() { return 1; }
auto g = [](auto x) { return x; };
  • 但此时 auto 仍然使用的是模板实参推断的机制,因此不能为 auto 返回类型返回一个初始化列表,即使是单元素
auto newInitList() { return { 1 }; } // 错误
  • 泛型 lambda 同理
std::vector<int> v { 2, 4, 6 };
auto resetV = [&v](const auto& newValue) { v = newValue; };
resetV({ 1, 2, 3 }); // 错误

C++17 的 auto

  • C++17中,auto 可以作为非类型模板参数
template<auto N>
struct X {
  void f() { std::cout << N; }
};

X<1> x;
x.f(); // 1

03 decltype

  • decltype 会推断出直觉预期的类型
const int i = 0; // decltype(i) 为 const int

struct Point {
  int x, y; // decltype(Point::x) 和 decltype(Point::y) 为 int
};

A a; // decltype(a) 为 A
bool f(const A& x); // decltype(x) 为 const A&,decltype(f) 为 bool(const A&)
if (f(a)) … // decltype(f(a)) 为 bool

int a[] {1, 2, 3}; // decltype(a) 为 int[3]
  • decltype 一般用来声明与参数类型相关的返回类型。比如下面模板的参数是容器和索引,而返回类型取决于元素类型
template<typename Container, typename Index>
auto f(Container& c, Index i) -> decltype(c[i])
{ // auto 不做任何事,只是表示使用类型推断,推断使用的是 decltype
  return c[i];
}
  • C++14 允许省略尾置返回类型,只留下 auto
template<typename Container, typename Index>
auto f(Container& c, Index i)
{
  return c[i];
}
  • 但直接使用会发现问题
std::vector<int> v;
…
f(v, 5) = 10; // 返回 v[5] 然后赋值为 10,但不能通过编译
  • operator[] 返回元素引用,类型为 int&,但 auto 推断为 int,因此上面的操作相当于给一个整型值赋值,显然是错误的
  • 为了得到期望的返回类型,需要对返回类型使用 decltype 的推断机制,C++14 允许将返回类型声明为 decltype(auto) 来实现这点
template<typename Container, typename Index>
decltype(auto) f(Container& c, Index i)
{
  return c[i];
}
  • decltype(auto) 也可以作为变量声明类型
int i = 1;
const int& j = i;
decltype(auto) x = j; // const int& x = j;
  • 但还有一些问题,容器传的是 non-const 左值引用,这就无法接受右值
std::vector<int> makeV(); // 工厂函数
auto i = f(makeV(), 5);
  • 为了同时匹配左值和右值而又不想重载,只需要模板参数写为转发引用
template<typename Container, typename Index>
decltype(auto) f(Container&& c, Index i)
{
  return std::forward<Container>(c)[i]; // 传入的实参是右值时,std::forward 将 c 转为右值
}

// C++11版本
template<typename Container, typename Index>
auto f(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i])
{
  authenticateUser();
  return std::forward<Container>(c)[i];
}

decltype的特殊情况

  • 如果表达式是解引用,decltype 会推断为引用类型
int* p; // decltype(*p) 是 int&
  • 赋值表达式会产生引用,类型为赋值表达式中左值的引用类型
int a = 0;
int b = 1;
decltype(a=1) c = b; // int&
c = 3;
std::cout << a << b << c; // 033
  • 如果表达式加上一层或多层括号,编译器会将其看作表达式,变量是一种可以作为赋值语句左值的特殊表达式,因此也得到引用类型。decltype((variable)) 结果永远是引用,declytpe(variable) 只有当变量本身是引用时才是引用
int i; // decltype((i)) 是 int&
  • 在返回类型为 decltype(auto) 时,这可能导致返回局部变量的引用
decltype(auto) f1()
{
  int x = 0;
  …
  return x; // decltype(x) 是 int,因此返回 int
}

decltype(auto) f2()
{
  int x = 0;
  …
  return (x); // decltype((x)) 是 int&,因此返回了局部变量的引用
}

04 查看推断类型的方法

  • 最简单直接的方法是在 IDE 中将鼠标停放在变量上

  • 利用报错信息,比如写一个声明但不定义的类模板,用这个模板创建实例时将出错,编译将提示错误原因
template<typename T>
class A;

A<decltype(x)> xType; // 未定义类模板,错误信息将提示x类型
// 比如对 int x 报错如下
error C2079: “xType”使用未定义的 class“A<int>”
template<typename T>
void f(T& x)
{
  std::cout << "T = " << typeid(T).name() << '\n';
  std::cout << "x = " << typeid(x).name() << '\n';
}
#include <boost/type_index.hpp>

template<typename T>
void f(const T& x)
{
  using boost::typeindex::type_id_with_cvr;
  std::cout << "T = " << type_id_with_cvr<T>().pretty_name() << '\n';
  std::cout << "x = " << type_id_with_cvr<decltype(x)>().pretty_name() << '\n';
}