对于下面的函数模板:
template <typename T>
void f(ParamType param);
f(expr);
有下面三种情况:
ParamType
是一个指针或者引用,但不是通用引用ParamType
是一个通用引用ParamType
既不是指针也不是引用
ParamType
是一个指针或者非通用引用
此时类型推导会是这样的:
- 如果
expr
是引用,那么忽略引用部分,即忽略引用性 - 然后
expr
的类型与ParamType
进行模式匹配来决定T
例如,对于下面的模板:
template <typename T>
void f(T& param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T = int, ParamType = int&
f(cx); // T = const int, ParamType = const int&
f(rx); // T = const int, ParamType = const int&
首先,为什么 x
会推导出来 int&
:一个左值引用可以直接绑定一个同类型的左值变量
其次,在第二个和第三个例子中保留了 const
,因为进行模式匹配时 T&
并没有 const
,所以在匹配 T
时保留了下来
再看下面的例子:
template <typename T>
void f(const T& param);
int x = 27;
const int cx = x;
const int& rx = x;
f(x); // T = int, ParamType = const int&
f(cx); // T = int, ParamType = const int&
f(rx); // T = int, ParamType = const int&
这里又有些不一样,但是符合直觉
对于指针来说也是如此:
template <typename T>
void f(T* param);
int x = 27;
const int* px = &x;
f(&x); // T = int, ParamType = int*
f(px); // T = const int, ParamType = const int*
引用折叠和通用引用
C++ 本质上不允许引用的引用,它引入了引用折叠
如果任一引用为左值引用,则结果为左值引用。否则(即,如果引用都是右值引用),结果为右值引用
通用引用的基础是引用折叠
T&&
有两种不同的意思:
- 右值引用(在不需要类型推导的情况下)
- 既可以是右值引用,也可以是左值引用。它可以绑定到
const
或者 non-const
的对象上,它们可以绑定到几乎任何东西
第二种情况出现在需要类型推导时(即 T
可以被推导成左值引用),如:
template<typename T>
void f(T&& param);
Widget var1;
auto&& var2 = var1; // auto = Widget&
f(var1); // T = Widget&
f(std::move(var1)); // T = Widget&&,注意 T 不是 Widget
对一个通用引用而言,类型推导是必要的,但是它还不够。引用声明的形式必须正确,并且该形式是被限制的。它必须恰好为 T&&
,如下面的就不行:
template <typename T>
void f(std::vector<T>&& param); // 右值引用
template <typename T>
void f(const T&& param); // 右值引用,即使一个简单的 const 修饰符的出现,也足以使一个引用失去成为通用引用的资格
有时虽然看到了 T&&
,但也不一定是通用引用,如:
template<typename T, typename Allocator = allocator<T>>
class vector {
public:
void push_back(T&& x); // 这里的 T 会在声明 vector 对象时确定,所以这里并没有发生类型推导
template <typename... Args>
void emplace_back(Args&&... args); // 这里会发生类型推导,所以是通用引用
...
};
ParamType
是一个通用引用
它的规则是:
- 如果
expr
是左值,那么T
和ParamType
都会被推断为左值引用(这是唯一一种T
被推导为引用的情况) - 如果
expr
是右值,那么使用第一种情况(ParamType
是一个指针或者非通用引用)推导规则
例如:
template <typename T>
void f(T&& param);
int x = 27;
const int cx = x;
const int& rx = cx;
f(x); // T = int&, ParamType = int&
f(cx); // T = const int&, ParamType = const int&
f(rx); // T = const int&, ParamType = const int&
f(27); // T = int, ParamType = int&&
ParamType
既不是指针也不是引用
当 ParamType
既不是指针也不是引用时,我们通过传值(pass-by-value)的方式处理,这意味着无论传递什么 param
都会成为它的一份拷贝
它的规则是:
- 如果
expr
是一个引用,那么忽略引用部分 - 如果忽略引用性后,
expr
是一个const
,那么忽略const
,同样地也忽略volatile
也就是忽略引用和顶层 const
例如:
template <typename T>
void f(T param);
int x = 27;
const int cx = x;
const int& rx = cx;
f(x); // T = int
f(cx); // T = int
f(rx); // T = int
const char* const ptr = "Hello World!";
f(ptr); // T = const char*,这里只能忽略顶层 const
数组实参
这里还有一些小细节值得注意,比如数组类型不同于指针类型
数组类型可以退化为指针类型,如:
const char name[] = "J. P. Briggs"; // const char[13]
const char* ptrToName = name; // const char*
对于函数的形参来说,下面两种声明等价:
void func(int param[]);
void func(int* param);
当数组被传给按值传递的模板时,也会被视为指针。但是当数组被传给按引用传递的模板时,会被视为数组引用。即:
template <typename T>
void f1(T param);
template <typename T>
void f2(T& param);
template <typename T>
void f3(T&& param);
const char name[] = "J. P. Briggs"; // const char[13]
f1(name); // T = const char*
f2(name); // T = const char (&)[13]
f3(name); // T = const char (&)[13]
也可以使用下面的模板来得到数组大小:
template <typename T, std::size_t N>
constexpr std::size_t arraySize(T (&)[N]) noexcept {
return N;
}
函数实参
在 C++ 中不只是数组会退化为指针,函数类型也会退化为一个函数指针
与上面的数组类似:
template <typename T>
void f1(T param);
template <typename T>
void f2(T& param);
template <typename T>
void f3(T&& param);
void func(int, double); // void(int, double)
f1(func); // T = void(*)(int, double)
f2(func); // T = void(&)(int, double)
f3(func); // T = void(&)(int, double)