考虑如下函数模板和调用语句:
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]; // 危险行为:返回对 局部变量 的引用
}