考虑如下函数模板和调用语句:
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
是它的类型,前者 1 + 1
,编译器通过比较 ParaType
与 ArguType
来推断 T
:
- 忽略
ArguType
的 RCV 属性,其中 R、C、V 分别表示:引用属性、顶层const
属性、顶层volatile
属性。- 将上一步所得类型与
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
按以下规则推断:
- 若
argument
为左值表达式,则T
为左值引用,否则T
不含引用。- 若上述推断结果含多重引用,则按引用折叠规则处理。
引用折叠 (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
:
ExprType
是 expr
的类型(含 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
}
如果希望以 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 允许以 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]; // 危险行为:返回对 局部变量 的引用
}