文章目录
左值和右值
在C++中,左值(lvalue)和右值(rvalue)是两种不同的表达式类型,它们的主要区别在于它们在内存中的状态和使用方式。
左值(Lvalue)
左值是指那些在内存中有持久存储位置的对象。它们通常代表对象的身份,即它们有一个明确的内存地址,并且可以通过这个地址进行读写操作。左值可以出现在赋值操作的左边或右边。
特征:
- 可以取地址(即可以使用
&
操作符获取其内存地址)。 - 可以被赋值。
- 可以作为非常量引用的绑定对象。
例子:
int a = 10; // 'a' 是一个左值,因为它有一个持久的存储位置。
int* p = &a; // 取'a'的地址,'p'现在指向'a'的存储位置。
a = 20; // 'a' 可以被赋值。
右值(Rvalue)
右值是指那些在内存中没有持久存储位置的对象,通常是临时的,比如字面量、表达式的计算结果等。右值代表的是值本身,而不是值所在的内存位置。右值不能被赋值,也不能取地址。
特征:
- 不能取地址(尝试获取右值的地址会导致编译错误)。
- 不能被赋值。
- 通常用作右值引用的绑定对象,以实现移动语义。
例子:
int b = 30; // 'b' 是一个左值。
int c = b * 2; // 'b * 2' 是一个右值,因为它是一个表达式的计算结果。
区别
- 持久性:左值指向内存中的持久对象,而右值通常是临时的,表达式结束后就会被销毁。
- 可变性:左值可以被重新赋值,而右值通常不能。
- 地址:左值可以取地址,而右值不可以。
左值和右值的核⼼区别就是能否取地址
左值引⽤和右值引⽤
左值引用(Lvalue Reference)
- 定义:左值引用用于引用可以取地址的变量,即具有持久性存储的对象。例如,变量、数组元素、解引用指针等都是左值。
- 语法:
Type& r1 = x;
这里的r1
是对b
的左值引用。 - 常见的左值引用:
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
double x = 1.1, y = 2.2;
右值引用(Rvalue Reference)
- 定义:右值引用用于引用那些临时对象或不可取地址的对象。右值通常是字面量、表达式结果等。
- 语法:
Type&& rr1 = 10;
这里的rr1
是对右值10
的右值引用。
右值引用的特点
- 不能直接引用左值:
- 右值引用不能绑定到左值,因为左值的生命周期比右值长。
- 左值引⽤不能直接引⽤右值,但是
<font style="color:rgb(31,35,41);">const</font>
左值引⽤可以引⽤右值 - 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤
<font style="color:rgb(31,35,41);">move(左值)</font>
- 例:
// int&& rrx1 = b; // 错误
// 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
const int& rx1 = 10;
const double& rx2 = x + y;
const double& rx3 = fmin(x, y);
const string& rx4 = string("11111");
- 可以引用通过
std::move
转换的左值: <font style="color:rgb(31,35,41);">move</font>
是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换<font style="color:rgb(31,35,41);">template <class T> typename remove_reference<T>::type&& move (T&& arg); </font>
std::move
将左值强制转换为右值引用,允许右值引用绑定到左值。例如:
int&& rrx1 = move(b); // 通过move将b转换为右值引用
- 变量表达式属性:
- 所有变量表达式(包括右值引用变量)都是左值属性,意味着它们可以被取地址。⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
string&& rr4 = string("11111");
也就是说以上的rr
皆为左值。
右值引用延长生命周期
右值引⽤可⽤于为临时对象延⻓⽣命周期,const 的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆
法被修改。
std::string s1 = "Test";
// std::string&& r1 = s1; // 错误:不能绑定到左值
const std::string& r2 = s1 + s1; // OK:到 const 的左值引用延⻓生存期
// r2 += "Test"; // 错误:不能通过到 const 的引⽤修改
std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期
r3 += "Test"; // OK:能通过到非const 的引⽤修改
std::cout << r3 << '\n';
- C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。
- C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。
void f(int& x)
{
std::cout << "左值引用重载 f(" << x << ")\n";
}
void f(const int& x)
{
std::cout << "到 const 的左值引用重载 f(" << x << ")\n";
}
void f(int&& x)
{
std::cout << "右值引用重载 f(" << x << ")\n";
}
int main()
{
int i = 1;
const int ci = 2;
f(i); // 调⽤ f(int&)
f(ci); // 调⽤ f(const int&)
f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)
f(std::move(i)); // 调⽤ f(int&&)
// 右值引⽤变量在⽤于表达式时是左值
int&& x = 1;
f(x); // 调⽤ f(int& x)
f(std::move(x)); // 调⽤ f(int&& x)
return 0;
}
右值引⽤和移动语义的使⽤ (重点)
左值引用的主要使用场景回顾
左值引用主要的使用场景是在函数中通过左值引用传递返回值的时候减少拷贝或者在传参的时候用左值引用接收实参减少拷贝,并且还可以修改接收的实参。
左值引用已经解决了大部分效率问题,但是在有些情况下还是无法完全解决并且可能造成错误。例如在addString
和generate
函数,如果使用左值引用接收返回的对象的话则会得到一个已经析构的对象,因为该对象已经离开了创建时所在的作用域,导致引用的空间也被释放。
string addStrings(string num1, string num2)
{
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
return str;
}
通过C++98的方法可以通过增加参数多传入一个提前创建好的对象的引用,然后在函数中直接对该对象进行构造来避免多次拷贝造成效率上的浪费。
string addStrings(string num1, string num2, string& str)
{
......
}
string str;
string addStrings(s1, s2, str); // 直接传入str在内部进行构造
那么在这个时候能用右值引用来解决吗?
上文已经提出:右值引用可以延长对象的生命周期,并且恰好可以直接返回右值来避免再次构造对象。
实践证明,使用右值引用来接收返回值则会收到空的内容。但是右值引用不是可以延长右值的生命周期吗,为什么还是内容被销毁。
实际上,右值引用确实可以延长右值的生命周期,但是返回的右值是在构造的函数栈帧中建立的空间,当使用完函数后栈帧会被释放,当然右值的空间也会被释放,所以即使接受了返回值,接收的也是空值。
所以可以引出移动语意。
移动构造函数与移动赋值操作符
定义
- 移动构造函数:
- 定义:移动构造函数接受一个右值引用作为参数,并通过“窃取”资源来初始化对象。
- 语法:
ClassName(ClassName&& other) noexcept
。 - 目的:避免不必要的深拷贝,提高性能。
- 移动赋值操作符:
- 定义:移动赋值操作符重载,允许将一个右值引用的对象赋值给当前对象。
- 语法:
ClassName& operator=(ClassName&& other) noexcept
。 - 目的:同样避免不必要的拷贝,提高效率。
代码示例
含有移动构造函数和移动赋值运算符重载的my_string
类模拟实现
class string {
public:
// 构造函数
string(const char* str = "")
: _size(strlen(str)), _capacity(_size) {
// 资源分配
}
// 拷贝构造函数
string(const string& s) : _str(nullptr) {
// 深拷贝实现
}
// 移动构造函数
string(string&& s) {
cout << "string(string&& s) -- 移动构造" << endl;
swap(s); // 窃取资源
}
// 移动赋值操作符
string& operator=(string&& s) {
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s); // 窃取资源
return *this;
}
// 交换成员函数
void swap(string& other) noexcept {
char* tmp = this->_str;
this->_str = other._str;
other._str = tmp;
}
// 析构函数
~string() {
delete[] _str; // 释放资源
}
};
- 构造函数:动态分配内存并初始化
_str
。 - 拷贝构造函数:实现深拷贝,通过逐字符复制。
- 移动构造函数:
swap(s)
窃取s
的资源,避免深拷贝。- 在完成构造后,
s
进入一个有效但未定义的状态。
- 移动赋值操作符:
- 同样使用
swap(s)
,在赋值前确保清理当前对象的资源。 - 通过
noexcept
,确保在发生异常时程序的安全性。
- 同样使用
测试main
函数:
int main() {
my_string::string s1("xxxxx");
my_string::string s2 = s1; // 拷贝构造
my_string::string s3 = my_string::string("yyyyy"); // 移动构造优化
my_string::string s4 = move(s1); // 移动构造
return 0;
}
- s1 的初始化:
my_string::string s1("xxxxx");
这行代码调用了构造函数,创建了一个新的 my_string
对象 s1
。这里使用的是普通构造函数,而不是移动构造。
- 拷贝构造:
my_string::string s2 = s1;
这行代码使用了拷贝构造函数,因为 s1
是一个左值(它有名字且可以取地址)。因此,拷贝构造函数被调用,复制 s1
的内容到 s2
。
- 移动构造优化:
my_string::string s3 = my_string::string("yyyyy");
这里,my_string::string("yyyyy")
是一个临时对象(右值),因此会触发移动构造函数的调用。编译器会优化这一步骤,直接通过移动构造来初始化 s3
。
- 移动构造:
my_string::string s4 = move(s1);
使用了 std::move
,这将 s1
转换为右值引用,使得移动构造函数被调用。此时,s1
的资源被“窃取”,而 s1
进入一个有效但未定义的状态。
右值引⽤和移动语义解决传值返回问题
#define _CRT_SECURE_NO_WARNINGS 1
#include<string>
#include<algorithm>
#include<assert.h>
#include <iostream>
using namespace std;
string&& addStrings(string num1, string num2)
{
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
return move(str);
}
namespace my_string
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
cout << "string(char* str)-构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 拷⻉构造" << endl;
reserve(s._capacity);
for (auto ch : s)
{
push_back(ch);
}
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
// 移动构造
string(string&& s)
{
cout << "string(string&& s) -- 移动构造" << endl;
swap(s);
}
void swap(string& other) noexcept
{
char* tmp = this->_str;
this->_str = other._str;
other._str = tmp;
}
~string()
{
cout << "~string() -- 析构" << endl;
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
if (_str)
{
strcpy(tmp, _str);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity *
2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
string addStrings(string num1, string num2)
{
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
cout << "******************************" << endl;
return str;
}
}
// 场景1
int main()
{
my_string::string ret = my_string::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
// 场景2
int main()
{
my_string::string ret;
ret = my_string::addStrings("11111", "2222");
cout << ret.c_str() << endl;
return 0;
}
右值对象构造,只有拷⻉构造,没有移动构造的场景
- 图1展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次拷⻉构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次拷⻉构造。
- 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰。
- linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elideconstructors 的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次拷⻉。
右值对象构造,有拷⻉构造,也有移动构造的场景
- 图2展⽰了vs2019 debug环境下编译器对拷⻉的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷⻉合⼆为⼀变为⼀次移动构造。
- 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码优化为⾮常恐怖,会直接将str对象的构造,str拷⻉构造临时对象,临时对象拷⻉构造ret对象,合三为⼀,变为直接构造。要理解这个优化要结合局部对象⽣命周期和栈帧的⻆度理解,如图3所⽰。
- linux下可以将下⾯代码拷⻉到test.cpp⽂件,编译时⽤ g++ test.cpp -fno-elideconstructors 的⽅式关闭构造优化,运⾏结果可以看到图1左边没有优化的两次移动。
图二
图三
右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景
- 图4左边展⽰了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次拷⻉构造,⼀次拷⻉赋值。
- 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景
- 图5左边展⽰了vs2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,⼀次移动构造,⼀次移动赋值。
- 需要注意的是在vs2019的release和vs2022的debug和release,下⾯代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引⽤,底层⻆度⽤指针实现。运⾏结果的⻆度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
右值引⽤和移动语义在传参中的提效
STL 容器中的右值引用:
在 STL 中,许多容器(如 std::list
、std::vector
等)增加了支持右值引用的接口:
- 当传入一个左值时,容器会调用拷贝构造函数。
- 当传入一个右值时,容器会调用移动构造函数,将右值的资源
swap
到当前对象上。
// void push_back (const value_type& val);
// void push_back (value_type&& val);
// iterator insert (const_iterator position, value_type&& val);
// iterator insert (const_iterator position, const value_type& val);
int main()
{
std::list<bit::string> lt;
bit::string s1("111111111111111111111");
lt.push_back(s1);
cout << "*************************" << endl;
lt.push_back(bit::string("22222222222222222222222222222"));
cout << "*************************" << endl;
lt.push_back("3333333333333333333333333333");
cout << "*************************" << endl;
lt.push_back(move(s1));
cout << "*************************" << endl;
return 0;
}
运⾏结果:
string(char* str)
string(const string& s) --拷⻉构造
*************************
string(char* str)
string(string && s) --移动构造
~string() --析构
*************************
string(char* str)
string(string && s) --移动构造
~string() --析构
*************************
string(string && s) --移动构造
*************************
~string() --析构
~string() --析构
~string() --析构
~string() --析构
~string() --析构
类型分类
在C++中,类型分类是一个重要的概念,它决定了对象的生命周期、存储方式以及它们在表达式中的行为。C++11标准引入了新的类型分类,以支持右值引用和移动语义。
左值(Lvalue)
左值是指具有明确存储位置的对象,它们通常代表对象的身份。左值可以出现在赋值操作的左右两边,并且可以取地址。
特征:
- 可以被赋值。
- 可以取地址。
- 代表对象的身份。
例子:
int a = 10; // 'a' 是一个左值,因为它有一个持久的存储位置。
int* p = &a; // 取'a'的地址,'p'现在指向'a'的存储位置。
a = 20; // 'a' 可以被赋值。
右值(Rvalue)
右值是指那些没有持久存储位置的对象,通常是临时的,比如字面量、表达式的计算结果等。右值代表的是值本身,而不是值所在的内存位置。
特征:
- 不能被赋值。
- 不能取地址。
- 代表值本身。
例子:
int b = 30; // 'b' 是一个左值。
int c = b * 2; // 'b * 2' 是一个右值,因为它是一个表达式的计算结果。
纯右值(Prvalue)
C++11中引入了纯右值的概念,它指的是那些字面量常量或求值结果相当于字面量或是一个个不具名的临时对象。
特征:
- 通常是临时对象或字面量。
- 不能被移动。
例子:
int x = 42; // '42' 是一个纯右值。
int y = x + 2; // 'x + 2' 也是一个纯右值。
将亡值(Xvalue)
将亡值是指那些即将被移动的对象,它们通常是通过右值引用返回的函数调用表达式或转换为右值引用的转换函数的调用表达。
特征:
- 可以被移动。
- 代表即将被移动的对象。
例子:
int&& func() {
int a = 10;
return std::move(a);
}
int&& x = func(); // 'func()' 返回的是一个将亡值。
泛左值(Glvalue)
泛左值是C++11中引入的一个更广泛的概念,它包括了左值和将亡值。泛左值可以表示对象的身份,并且可以被取地址。
特征:
- 包含左值和将亡值。
- 可以被取地址。
例子:
int a = 10; // 'a' 是一个泛左值,因为它是一个左值。
int&& b = std::move(a); // 'b' 也是一个泛左值,因为它是一个将亡值。
引用折叠
什么是引用折叠?
引用折叠指的是当我们使用模板和类型别名(typedef
)时,组合不同类型的引用会产生新的引用类型。C++11 规定了一些折叠规则来处理这些情况:
- 右值引用的右值引用折叠成右值引用。
- 所有其他组合(如左值引用与右值引用、左值引用与左值引用等)都折叠成左值引用。
为什么需要引用折叠?
在 C++ 中,引用的作用是为了避免不必要的拷贝,直接操作原对象。引用折叠使得在模板中使用引用时,能根据实际传入的参数类型自动决定使用左值引用还是右值引用,从而提高性能。
引用折叠的应用示例
函数模板
在函数模板中,T&&
是一种万能引用(或转发引用),根据传入的参数类型,**T**
** 会推导为左值引用或右值引用**。如下所示:
f1(T& x)
总是实例化为左值引用,因为无论传入的是左值还是右值,T&
都不发生变化。f2(T&& x)
根据传入的参数类型,实例化为左值引用或右值引用。例如,传入int&
时,f2
实例化为void f2(int& x)
;传入int
时,实例化为void f2(int&& x)
。
// 由于引用折叠规则,f1模板实例化后总是一个左值引用
template<class T>
void f1(T& x)
{}
// 由于引用折叠规则,f2模板实例化后可以是左值引用或右值引用
template<class T>
void f2(T&& x)
{}
// 没有折叠,实例化为 void f1(int& x)
// n 是左值,绑定到 T 的左值引用(即 T=int),故 f1<int>(n) 成功
f1<int>(n);
// 报错:0 是右值,不能绑定到左值引用
f1<int>(0); // 报错
// 折叠,实例化为 void f1(int& x)
// n 是左值,T 推导为 int&,故实例化成功
f1<int&>(n);
// 报错:0 是右值,不能绑定到左值引用
f1<int&>(0); // 报错
// 折叠,实例化为 void f1(int& x)
// n 是左值,T 推导为 int&&,因此实例化为左值引用
f1<int&&>(n); // 报错: 左值不能绑定到右值引用
// 报错:0 是右值,不能绑定到左值引用
f1<int&&>(0); // 报错
// 折叠,实例化为 void f1(const int& x)
// n 是左值,T 推导为 const int&,故实例化成功
f1<const int&>(n);
// 报错:0 是右值,不能绑定到 const 左值引用
f1<const int&>(0); // 报错
// 折叠,实例化为 void f1(const int& x)
// n 是左值,T 推导为 const int&&,因为 const 的左值引用会折叠成左值引用
f1<const int&&>(n);
// 报错:0 是右值,不能绑定到 const 左值引用
f1<const int&&>(0); // 报错
// 没有折叠,实例化为 void f2(int&& x)
// n 是左值,不能绑定到右值引用,因此报错
f2<int>(n); // 报错
// 报错:0 是右值,无法绑定到右值引用
f2<int>(0); // 报错
// 折叠,实例化为 void f2(int& x)
// n 是左值,T 推导为 int&,所以实例化成功
f2<int&>(n);
// 报错:0 是右值,无法绑定到左值引用
f2<int&>(0); // 报错
// 折叠,实例化为 void f2(int&& x)
// n 是左值,不能绑定到右值引用,因此报错
f2<int&&>(n); // 报错
// 报错:0 是右值,能够绑定到右值引用,因此实例化成功
f2<int&&>(0); // 报错
示例2:
template<class T>
void Function(T&& t) // T 是万能引用(转发引用),会根据实参推导类型
{
int a = 0; // 定义一个整数 a
T x = a; // x 的类型根据 T 的推导结果而定
// x++ 可能会报错,因为 x 的类型可能是 const 引用
cout << &a << endl; // 输出 a 的地址
cout << &x << endl; // 输出 x 的地址
cout << endl;
}
int main()
{
// 10 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)
Function(10); // 右值
int a; // 定义一个整数 a
// a 是左值,推导出 T 为 int&,引用折叠,模板实例化为 void Function(int& t)
Function(a); // 左值
// std::move(a) 是右值,推导出 T 为 int,模板实例化为 void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8; // 定义一个常量整数 b
// b 是 const 左值,推导出 T 为 const int&,引用折叠,模板实例化为 void Function(const int& t)
// 因为 t 是 const 引用,x 也会是 const 引用,因此 x++ 会报错
Function(b); // const 左值
// std::move(b) 是右值,推导出 T 为 const int,模板实例化为 void Function(const int&& t)
// 因为 t 是 const 引用,x 也会是 const 引用,因此 x++ 会报错
Function(std::move(b)); // const 右值
return 0;
}
typedef 引用折叠
在 typedef
或 using
中定义的引用类型同样遵循引用折叠规则。lref
和 rref
的实例化表现如下:
lref&
和lref&&
都会折叠成int&
,即左值引用。rref&
报错,因为它是引用的引用,最终折叠为左值引用。
typedef int& lref; // lref = int&
typedef int&& rref; // rref = int&&
lref& r1 = n; // OK: r1 是 int&
lref&& r2 = n; // OK: r2 是 int&
rref& r3 = n; // 报错: rref 是 int&&,不能绑定到 int&
rref&& r4 = 1; // OK: r4 是 int&&
完美转发完美转发:保持函数参数的值类别
在 C++ 中,完美转发允许我们在模板中准确地转发参数的值类别(左值或右值)。通过完美转发,我们可以确保在函数内部调用其他函数时,参数的性质(左值或右值)不会丢失。
完美转发的背景
考虑以下情况:
- 我们定义了一个函数模板
<font style="color:rgb(31,35,41);">Function</font>
,它接受一个参数<font style="color:rgb(31,35,41);">T&& t</font>
。当传入一个右值时,<font style="color:rgb(31,35,41);">T</font>
会被推导为一个右值引用类型,而当传入一个左值时,<font style="color:rgb(31,35,41);">T</font>
会被推导为左值引用类型。 - 然而,在函数内部,
<font style="color:rgb(31,35,41);">t</font>
的值类别是左值,这会导致在调用另一个函数时只会匹配左值引用版本的函数。
为了保持参数的值类别,我们需要使用 <font style="color:rgb(31,35,41);">std::forward</font>
函数进行完美转发。
<font style="color:rgb(31,35,41);">std::forward</font>
的实现
<font style="color:rgb(31,35,41);">std::forward</font>
是一个函数模板,定义如下:
template <class T>
T&& forward(typename remove_reference<T>::type& arg) noexcept;
template <class T>
T&& forward(typename remove_reference<T>::type&& arg) noexcept;
它的作用是将参数转换为其原始的值类别。<font style="color:rgb(31,35,41);">remove_reference</font>
用于移除引用,确保我们能够正确处理参数类型。
示例代码分析
让我们通过一个代码示例来理解完美转发的实现和使用。
#include <iostream>
using namespace std;
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<class T>
void Function(T&& t) {
// 这里直接传递 t,会导致 t 的值类别变为左值
// Fun(t);
// 使用 std::forward 保持 t 的原始值类别
Fun(std::forward<T>(t));
}
int main() {
Function(10); // 右值
int a;
Function(a); // 左值
Function(std::move(a)); // 右值
const int b = 8;
Function(b); // const 左值
Function(std::move(b)); // const 右值
return 0;
}
流程分析
- 右值传递:
Function(10); // 右值
<font style="color:rgb(31,35,41);">T</font>
被推导为<font style="color:rgb(31,35,41);">int</font>
,所以<font style="color:rgb(31,35,41);">Function</font>
实例化为<font style="color:rgb(31,35,41);">void Function(int&& t)</font>
,在<font style="color:rgb(31,35,41);">Function</font>
中<font style="color:rgb(31,35,41);">t</font>
变为了左值。- 使用
<font style="color:rgb(31,35,41);">std::forward<T>(t)</font>
将<font style="color:rgb(31,35,41);">t</font>
作为右值转发给<font style="color:rgb(31,35,41);">Fun</font>
,匹配<font style="color:rgb(31,35,41);">Fun(int&& x)</font>
。
- 左值传递:
int a;
Function(a); // 左值
<font style="color:rgb(31,35,41);">T</font>
被推导为<font style="color:rgb(31,35,41);">int&</font>
,实例化为<font style="color:rgb(31,35,41);">void Function(int& t)</font>
。- 使用
<font style="color:rgb(31,35,41);">std::forward<T>(t)</font>
,<font style="color:rgb(31,35,41);">t</font>
作为左值转发给<font style="color:rgb(31,35,41);">Fun</font>
,匹配<font style="color:rgb(31,35,41);">Fun(int& x)</font>
。
- 使用
<font style="color:rgb(31,35,41);">std::move</font>
转发:
Function(std::move(a)); // 右值
<font style="color:rgb(31,35,41);">std::move(a)</font>
将<font style="color:rgb(31,35,41);">a</font>
转换为右值,<font style="color:rgb(31,35,41);">T</font>
被推导为<font style="color:rgb(31,35,41);">int</font>
。<font style="color:rgb(31,35,41);">std::forward<T>(t)</font>
将<font style="color:rgb(31,35,41);">t</font>
作为右值转发,匹配<font style="color:rgb(31,35,41);">Fun(int&& x)</font>
。
- 处理常量左值:
const int b = 8;
Function(b); // const 左值
<font style="color:rgb(31,35,41);">T</font>
被推导为<font style="color:rgb(31,35,41);">const int&</font>
,实例化为<font style="color:rgb(31,35,41);">void Function(const int& t)</font>
。- 转发时,匹配
<font style="color:rgb(31,35,41);">Fun(const int& x)</font>
。
- 处理常量右值:
Function(std::move(b)); // const 右值
<font style="color:rgb(31,35,41);">std::move(b)</font>
将<font style="color:rgb(31,35,41);">b</font>
转换为右值,<font style="color:rgb(31,35,41);">T</font>
被推导为<font style="color:rgb(31,35,41);">const int</font>
。- 转发时,匹配
<font style="color:rgb(31,35,41);">Fun(const int&& x)</font>
。