模板中的右值引用(万能引用)、引用折叠与完美转发
文章目录
- 模板中的右值引用(万能引用)、引用折叠与完美转发
- 一、万能引用与引用折叠
- 1. 模板中的右值引用
- 2. 自动类型推导(auto)与万能引用
- 3. 引用折叠与万能引用
- 4. lambda表达式捕获
- 5. 条件转发
- 6. 类型萃取
- 二、完美转发
- 总结
一、万能引用与引用折叠
1. 模板中的右值引用
我们经常听万能引用,什么是万能引用,与普通的右值引用有什么区别?
下面给出一个案例,利用模板函数参数中的右值引用来展现万能引用的用途:
首先给出一个自定义的类型,简易的实现出拷贝构造函数和移动构造函数同时不必关注是否正常实现了功能,增加打印信息以便我们可以通过控制台明晰该函数是否被调用,何时被调用:
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
}
Date(Date&& d)
{
cout << "Date(Date&& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
接着给出两个测试函数,函数参数类型分别常量左值引用和右值引用:
// 常量左值引用函数模板
template<typename T>
void func1(const T& val)
{
T d(val);
}
// 万能引用参数函数模板
template<typename T>
void func2(T&& val)
{
T d(std::forward<T>(val));
cout << "val是右值引用:" << std::is_rvalue_reference<decltype(val)>::value << endl;
}
注意上面 func2() 函数中用到的 forward() 相当于 move() 的进阶版,可以保证在函数间参数传递时保持原有左/右值类型。
继续给出测试用例:
Date d(2000, 1, 2);
cout << "*********左值参数********" << endl;
func1(d);
cout << "*************************" << endl;
func2(d);
cout << "*********右值参数********" << endl;
func1(std::move(d));
cout << "*************************" << endl;
func2(std::move(d));
运行结果:
通过上图我们认识到万能引用在模板中的重要性,万能引用是模板中大多数情况保证移动构造函数被正常调用的重要条件。只有参数是万能引用时,函数内部才可调用 std::forward<T>()
完美转发变量的左右值类型,并将保持源类型的变量转发给其他函数。
至于我们为什么称其为“万能引用”,虽然常量左值引用 “const T& val” 也能兼容接收左值和右值参数,但它不支持移动语义,因为它不允许修改绑定的对象。因此,万能引用更有优势,因为它
既支持移动语义,又能与完美转发结合使用
,成功传递变量及其类型。
2. 自动类型推导(auto)与万能引用
当使用auto
关键字时,万能引用可以帮助推导变量的类型。例如,auto&&
可以根据初始化表达式是左值还是右值来推导出正确的类型。
void test2()
{
Date d(2000, 1, 2);
auto&& d1(d);
cout << "d1是左值引用:" << std::is_lvalue_reference<decltype(d1)>::value << endl;
cout << "d1是右值引用:" << std::is_rvalue_reference<decltype(d1)>::value << endl;
auto&& d2(std::move(d));
cout << "d2是左值引用:" << std::is_lvalue_reference<decltype(d2)>::value << endl;
cout << "d2是右值引用:" << std::is_rvalue_reference<decltype(d2)>::value << endl;
}
通过上图我们发现 auto&&
可以在推导表达式类型时,同时推导其左值性或右值性。
3. 引用折叠与万能引用
引用折叠的规则解决了引用的引用(C++中不允许)的问题。这里是引用折叠的基本规则:
- 如果两个引用中至少一个是左值引用(
&
),那么结果是左值引用(&
)。- 如果两个引用都是右值引用(
&&
),那么结果是右值引用(&&
)。
首先给出一个简单的以万能引用为参数的模版函数:
template<typename T>
void func(T&& val) {
// val 是一个万能引用
auto p = &val;
cout << "左值引用:" << std::is_lvalue_reference<decltype(val)>::value << endl;
cout << "右值引用:" << std::is_rvalue_reference<decltype(val)>::value << endl;
}
测试函数:
void test3()
{
int x = 10;
int& lx = x; // lx 是 x 的左值引用
int&& rx = 10; // rx 是一个右值引用
func(x); // T 推导为 int&,因此 val 的类型为 int& &,折叠为 int&
cout << "x是右值引用:" << std::is_rvalue_reference<decltype(x)>::value << endl;
cout << "-------------------" << endl;
func(lx); // T 推导为 int&,因此 val 的类型为 int& &,折叠为 int&
cout << "lx是右值引用:" << std::is_rvalue_reference<decltype(lx)>::value << endl;
cout << "-------------------" << endl;
func(rx); // T 推导为 int&&,因此 val 的类型为 int&& &&,折叠为 int&&
cout << "xr是右值引用:" << std::is_rvalue_reference<decltype(rx)>::value << endl;
cout << endl;
func(move(x));
cout << "move(x)是右值引用:" << std::is_rvalue_reference<decltype(move(x))>::value << endl;
cout << "-------------------" << endl;
func(10); // T 推导为 int,因此 val 的类型为 int&&
}
运行结果:
对上面运行结果部分地方需要着重做出解释:
4. lambda表达式捕获
void test4()
{
auto x = 5;
auto lambda = [y = std::move(x)]() mutable {
y += 2;
return y;
};
cout << x << endl;
cout << lambda() << endl;
}
当调用 lambda()
时,将输出 7,因为 y
被初始化为 5 并且增加了 2。请注意,由于 x
被移动到 y
,x
的值在移动后未定义,但通常在实际编译器实现中,基本类型的值在移动后保持不变。因此,输出 x
的值仍然是 5。
5. 条件转发
template<typename T>
void forwarder(T&& arg) {
if constexpr (std::is_lvalue_reference_v<T>) {
//process(arg); // 处理左值
}
else {
//process(std::move(arg)); // 处理右值
}
}
在这个例子中,forwarder
函数使用万能引用 T&&
来接受任何类型的参数,并根据参数的类型来决定调用哪个 process
函数。
6. 类型萃取
类型萃取(Type Traits)是模板元编程中的一种技术,它允许你在编译时检查类型信息或者修改类型。万能引用可以与类型萃取结合使用,以确定传递给模板的参数类型的属性。
template<typename T>
void process(T&& arg) {
using Type = typename std::remove_reference<T>::type;
if constexpr (std::is_integral_v<Type>) {
// 如果 T 是整数类型
//handle_integral(std::forward<T>(arg));
}
else {
// 如果 T 不是整数类型
//handle_non_integral(std::forward<T>(arg));
}
}
在这个例子中,process
函数使用类型萃取来移除引用,并检查 T
是否为整数类型。然后它使用 std::forward
来保持参数的值类别,并将其传递给相应的处理函数。
二、完美转发
实际上这里才真正提起完美转发这个概念,看了本文前面内容也已经大概了解,我们这里仅仅将其抽离出来进行特别总结。
-
首先,我们已经知道
右值引用变量具有左值属性
,因为其需要保留可修改性,所以自然不能是右值。 -
其次,传递给右值引用类型的参数在
传参后会退化为左值
。
所以我们想要在万能引用作参数的函数内部实现分离操作,需要对参数的左右值属性进行判断,例如:
template<typename T>
void print_right(T&& v)
{
cout << "right -> " << v << endl;
}
template<typename T>
void print_left(const T& v)
{
cout << "left -> " << v << endl;
}
template<typename T>
void do_something(T&& val)
{
if (std::is_rvalue_reference<decltype(val)>::value) // val是右值
{
print_right(std::forward<T>(val)); // 利用完美转发将退化逆向为初始类型,此处判断初始右值,也可 move(val)
}
else // val是左值
{
print_left(val); // val退化为左值,直接传递
}
}
测试函数:
void test5()
{
int x = 18;
//do_something<int>(x); // 编译报错
do_something<int&>(x); // 注意这里x是左值,所以模板参数为int编译会报错,模板参数和函数参数类型需要统一满足引用折叠
do_something<int>(10);
}
注意上面测试代码的案例也是引用折叠的重要体现
对于完美转发 forward(val) 中 val 的属性可能是左值也可能是右值:
- 当 val 是左值时,forward 对
左值 val 不做处理
; - 当 val 是右值时,forward 的作用相当于
move(val)
。
总结
本文被三个词语所贯穿:万能引用、引用折叠、完美转发。这三个概念之间的联系紧密以及使用场景高度重合,正是因为C++11中提出如此富有意义的新概念,极大地方便了我们重构代码,理想高效地编码实现功能。