Effective Modern C++ Item 1

Understand template type deduction

auto是建立在模板类型推导的基础上的,这项条款能帮助理解auto基于的模板类型推导。

我们将基于这个模板进行讨论:

1
2
3
4
template<typename T>
void f(ParamType param);

f(expr); // call f with some expression

在编译期间,编译器使用expr进行两个类型推导:一个是针对T的,另一个是针对ParamType的。这两个类型通常是不同的,因为ParamType包含一些修饰,比如const和引用修饰符。在这种情况下,ParamTypeconst T&

T的类型推导不仅取决于expr的类型,也取决于ParamType的类型。这里有三种情况:

第一种情况: ParamType是引用或者指针, 但不是通用引用

规则是:

  • 如果 expr 的类型是引用, 则忽略引用部分.
  • 然后将 expr 的类型与 ParamType 进行模式匹配以确定 T.
1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void f(T& param);

int x = 27;
const int cx = x;
const int& rx = x;

f(x); //T是int,param的类型是int&
f(cx); //T是const int,param的类型是const int&
f(rx); //T是const int,param的类型是const int&

如果我们将f的形参类型T&改为const T&,情况有所变化,cxrxconstness依然被遵守,但是因为假设param是reference-to-constconst不再被推导为T的一部分:

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
void f(const T& param); //param现在是reference-to-const

int x = 27; //如之前一样
const int cx = x; //如之前一样
const int& rx = x; //如之前一样

f(x); //T是int,param的类型是const int&
f(cx); //T是int,param的类型是const int&
f(rx); //T是int,param的类型是const int&

如果param是一个指针(或者指向const的指针)而不是引用,情况本质上也一样:

1
2
3
4
5
6
7
8
9
template<typename T>
void f(T* param); //param现在是指针

int x = 27; //同之前一样
const int *px = &x; //px是指向作为const int的x的指针

f(&x); //T是int,param的类型是int*
f(px); //T是const int,param的类型是const int*

第二种情况: ParamType是通用引用

规则是:

  • 如果 expr 是左值,则 TParamType 都被推导为左值引用.
  • 如果 expr 是右值,则适用第一种情况规则.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename T>
void f(T&& param); //param现在是一个通用引用类型

int x=27; //如之前一样
const int cx=x; //如之前一样
const int & rx=cx; //如之前一样

f(x); //x是左值,所以T是int&,
//param类型也是int&

f(cx); //cx是左值,所以T是const int&,
//param类型也是const int&

f(rx); //rx是左值,所以T是const int&,
//param类型也是const int&

f(27); //27是右值,所以T是int,
//param类型就是int&&

当通用引用被使用时,类型推导会区分左值实参和右值实参。

第三种情况: ParamType既不是指针也不是引用

在这种情况下, 参数以传值(pass-by-value)的方式处理。无论传递什么,param都会成为它的一份拷贝——一个完整的新对象。

规则是:

  • 如果 expr\ 的类型是引用, 则忽略引用部分。
  • 在忽略 expr 的引用性之后, 也忽略任何 constvolatile
1
2
3
4
5
6
7
8
9
10
11
12
template<typename T>
void f(T param); // param is now pass-by-value(按值传递)

int x = 27;
const int cx = x;
const int& rx = x;
const char* const ptr = "Fun with pointers"; //ptr是一个常量指针,指向常量对象

f(x); //T和param的类型都是int
f(cx); //T和param的类型都是int
f(rx); //T和param的类型都是int
f(ptr); //传递const char * const类型的实参

即使cxrx表示const值,param也不是constparam是一个完全独立于cxrx的对象。

数组和函数参数的衰减

当我们将数组名称作为参数传递给函数时,它实际上会衰减为指向数组中第一个元素的指针。

1
2
3
4
5
6
template<typename T>
void f(T param); // pass-by-value

const char name[] = "J. P. Briggs"; //name的类型是const char[13]

f(name); //name是一个数组,但是T被推导为const char*

但是如果添加对数组的引用, 就可以推断出数组类型。

1
2
3
4
template<typename T>
void f(T& param);

f(name); //传数组给f

不只是数组会退化为指针,函数类型也会退化为一个函数指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void someFunc(int, double);         //someFunc是一个函数,
//类型是void(int, double)

template<typename T>
void f1(T param); //传值给f1

template<typename T>
void f2(T & param); //传引用给f2

f1(someFunc); //param被推导为指向函数的指针,
//类型是void(*)(int, double)
f2(someFunc); //param被推导为指向函数的引用,
//类型是void(&)(int, double)

重点

  • 在模板类型推导期间, 作为引用的参数被视为非引用, 即它们的引用性会被忽略。
  • 在推导通用引用参数的类型时, 左值参数得到特殊处理。
  • 在推导按值传递参数的类型时, constvolatile参数被视为非const非volatile
  • 在模板类型推导期间, 数组或函数名的参数会衰退为指针, 除非它们被用于初始化引用。