文章目录
- 前提
- 万能引用
- 为什么需要万能引用
- 引用折叠
- 完美转发
- `std::forward`
- 基本用法
- 参考链接
前提
在看别人写的一些库时,总是会碰见万能引用,引用折叠,完美转发这几个概念,这次对它们做一个详细的整理。
万能引用
万能引用 是一个特殊的引用类型,经常在模版编程中使用。它即可以绑定到左值,也可以绑定到右值,也就是说,使用万能引用的函数参数即可以接受左值,也可以接受右值。
万能引用是通过以下条件识别的:
- 在函数模版中,参数类型是
T&&
的形式 T
是一个模版参数
当这两个条件都满足的时候,T&&
被称为万能引用。万能引用的特别之处在于它可以绑定到左值、右值以及常量对象和非常量对象。
一个示例如下:
#include <iostream>
#include <utility> // for std::forward// 使用万能引用的模板函数
template<typename T>
void forwarder(T&& arg) { // T&& 被称为万能引用std::cout << "num: " << arg << std::endl;
}int main() {int a = 42;forwarder(a); // 即可以传递左值forwarder(42); // 也可以传递右值return 0;
}
为什么需要万能引用
万能引用的主要用途是实现完美转发。接下来我们介绍完美转发。
引用折叠
在介绍完美转发之前,我们先介绍一个完美转发中的概念——引用折叠。
一个模版函数,根据定义的形参和传入的实参的类型,我们可以有下面四种组合:
- 函数定义的形参类型是左值引用
T&
,传入的实参是左值引用&
- 函数定义的形参类型是左值引用
T&
,传入的实参是左值引用&&
- 函数定义的形参类型是右值引用
T&&
,传入的实参是左值引用&
- 函数定义的形参类型是右值引用
T&&
,传入的实参是右值引用&&
所有的引用折叠最终都代表一个引用,要么是左值引用,要么是右值引用。引用折叠的规则很简单:如果任一引用为左值引用,则结果为左值引用。只有两个都是右值引用,结果才为右值引用。
所以上述四种组合经过折叠后,结果如下:
T& &
折叠为T&
T& &&
折叠为T&
T&& &
折叠为T&
T&& &&
折叠为T&&
我们来看一个例子:
#include <iostream>// 泛型函数,展示引用折叠
template<typename T>
void referenceCollapsing(T&& arg) {// 打印参数类型if constexpr (std::is_lvalue_reference<decltype(arg)>::value) {std::cout << "arg is an lvalue reference" << std::endl;}else if constexpr (std::is_rvalue_reference<decltype(arg)>::value) {std::cout << "arg is an rvalue reference" << std::endl;}else {std::cout << "arg is not a reference" << std::endl;}
}int main() {int x = 42;referenceCollapsing(x); // x 是左值,T 推导为 int&,折叠后arg的类型为 int&referenceCollapsing(42); // 42 是右值,T 推导为 int, 折叠后arg的类型为 int&&return 0;
}
上面的这个例子应该能说明折叠引用的问题。
接下来看这份代码:
#include <iostream>
#include <utility>void process(int& x) {std::cout << "Processing lvalue: " << x << std::endl;
}void process(int&& x) {std::cout << "Processing rvalue: " << x << std::endl;
}template<typename T>
void forwarder(T&& arg) {process(arg);
}int main() {int a = 5;forwarder(a); forwarder(10); return 0;
}
简单分析下:forwarder
模版函数使用了万能引用。在main
函数中,调用了两次forward
函数,一次传入的参数是左值a,一次传入的参数是右值10。同时重载了process
函数,一个形参类型是int&,一个形参类型是int&&。
代码运行的结果如下:
Processing lvalue: 5
Processing lvalue: 10
当传入参数为左值a
时,运用引用折叠的规则,此时arg
的类型为int&,所以调用process(int& x)
;
当当传入参数为右值10
时,运用引用折叠的规则,此时arg
的类型为int&&,应该调用 。process(int&& x)
但是从结果来看,第二次调用了process(int& x)
。为什么呢?
因为在forwarder
的函数体内,虽然arg
的类型为右值引用,但是它也有了变量名称,也可以取地址,所以当forwarder
函数内部调用其它函数并将arg
作为参数时,此时arg
变为了右值。
那么如何解决这个问题呢?这就要用到完美转发了。
完美转发
完美转发(Perfect Forwarding)是C++11引入的一种技术,用于在函数模板中将参数“完美地”传递给另一个函数。这意味着参数的值类别(左值或右值)和所有修饰符(如 const、volatile 等)都能被保留,从而使被调用的函数能够正确处理参数。
std::forward
std::forward
是 C++11 引入的一个标准库函数模板,用于在泛型编程中实现完美转发(perfect forwarding)。它的主要作用是保持传入参数的值类别(左值或右值)不变地传递给另一个函数。
基本用法
测试代码如下:
#include <iostream>
#include <utility> // 包含 std::forward
#include <type_traits>// 普通函数重载,分别处理左值和右值
void process(int& x) {std::cout << "Processing lvalue: " << x << std::endl;
}void process(int&& x) {std::cout << "Processing rvalue: " << x << std::endl;
}// 泛型函数,用于转发参数
template <typename T>
void forwarder(T&& arg) {process(std::forward<T>(arg));
}int main() {int a = 5;forwarder(a); // 传递左值forwarder(10); // 传递右值return 0;
}
运行结果为:
Processing lvalue: 5
Processing rvalue: 10
这下就对了。
参考链接
- C++中的万能引用和完美转发