一、什么是左值与右值
左值是指在内存中有确定地址的对象,它可以在赋值语句的左边使用。左值的特点包括:
- 左值可以出现在赋值符号=的左边;
- 可以获取左值的地址;
- 左值是持久存在的,生命周期可以跨越多个表达式。
例如,下面的代码中,a 和 c 都是左值:
int a = 1;
int b = 2;
int c = a + b;
右值则是指在内存中没有确定地址的临时数据,它只能在表达式的右边使用。右值的特点包括:
- 右值不能被取地址;
- 右值通常只是临时存在的,生命周期只能在当前表达式中存在;
- 右值可以是一个变量、常量、字面量或者表达式,例如5、2.5、函数的返回值等。
例如,下面的代码中,2 和 (a + b) 都是右值:
int a = 1;
int b = 2;
int c = 2; // 2是一个右值
int d = a + b; // (a + b)是一个右值
对于大多数类型的对象,它们可以用作左值和右值。例如,一个整数变量既可以在赋值语句的左边使用,也可以在表达式的右边使用。然而,一些特殊类型只能作为左值或者右值。例如:
- 纯右值引用(从 C++11 开始):只能作为右值使用的引用类型;
- constexpr 变量(从 C++11 开始):只能用作右值;
- 关键字 this:只能用作左值。
更深一层,可以将 L-value 的 L, 理解成 Location,表示定位,地址。将 R-value 的 R 理解成 Read,表示读取数据。
二、左值引用与右值引用
左值引用:其实就是绑定到左值的引用,通过&来获得左值引用。
左值引用的基本语法:
type &引用名 = 左值表达式;
请根据以上概念仔细领悟以下实例.
int a = 3;
const int b = 5;
a = b + 2; // a是左值,b + 2是右值
b = a + 2; // 错!b是只读的左值,无写入权,不能出现在赋值符号左边
(a = 4) += 28; // a = 4是左值表达式,28是右值,+= 为赋值操作符
34 = a + 2; // 错!34是字面量不能做左值
int &r = a; // 正确,r引用a
右值引用:为了支持移动操作,C++11引入了一种新的引用类型—右值引用。所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。如我们将要看到的,右值引用有一个重要的性质——只能绑定到一个将要销毁的对象。 因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
右值引用的基本语法:
type &&引用名 = 右值表达式;
右值引用的“&&”中间不可以有空格。请根据以上概念仔细领悟以下实例
int i = 42;
int &r = i; // 正确,r引用i
int &&rr = i // 错误,不能将一个右值引用绑定到一个左值上
int &r2 = i * 42; // 错误,i*42是一个右值
const int &r3 = i * 42; //正确,我们可以将一个const的引用绑定到一个右值上
int &&r2 = i * 42; //正确,将r2绑定到乘法结果上
三、标准库std::move函数
虽然不能将一个右值引用直接绑定到一个左值上,但可以显式地将一个左值转换为对应的右值引用类型。我们通过调用一个名为std::move的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件utility中。
int rr1 = 10;
int &&rr2 = rr1; // 错误
int &&rr3 = std::move(rr1); // OK, 将左值绑定到右值引用上
std::move调用告诉编译器:我们有一个左值,但我们希望像右值一样处理它。我们必须认识到,调用move就意味着承诺:除了对rr1赋值或者销毁之外,我们将不再使用它。在调用move之后,我们不能对移后源对象的值做任何修改。
注意:我们可以销毁一个移后源对象,也可以赋予它新值,但是不能使用一个移后源对象的值。
实例
#include <iostream>
#include <utility>
#include <vector>
#include <string>
int main()
{
std::string str = "Hello";
std::vector<std::string> v;
//调用常规的拷贝构造函数,新建字符数组,拷贝数据
v.push_back(str);
std::cout << "After copy, str is \"" << str << "\"\n";
//调用移动构造函数,掏空str,掏空后,最好不要使用str
v.push_back(std::move(str));
std::cout << "After move, str is \"" << str << "\"\n";
std::cout << "The contents of the vector are \"" << v[0]
<< "\", \"" << v[1] << "\"\n";
}
std::move 的函数原型定义
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
四、标准库std::forward函数
std::forward通常是用于完美转发的,它会将输入的参数原封不动地传递到下一个函数中,这个“原封不动”指的是,如果输入的参数是左值,那么传递给下一个函数的参数的也是左值;如果输入的参数是右值,那么传递给下一个函数的参数的也是右值
一个经典的完美转发的场景是:
template <class... Args>
void forward(Args&&... args) {
f(std::forward<Args>(args)...);
}
需要注意输入参数的类型是Args&&... , &&的作用是引用折叠,其规则是:
&& && -> &&
& && -> &
& & -> &
&& & -> &
由于我们需要保留输入参数的右值属性,因此Args后面需要跟上&&;2、std::forward的模板参数必须是<Args>,而不能是<Args...>,这是由于我们不能对Args进行解包之后传递给std::forward,而解包的过程必须在调用std::forward之后.
std::forward 的函数原型定义
template<class T>
constexpr T&& forward(std::remove_reference_t<T>& arg) noexcept{
// forward an lvalue as either an lvalue or an rvalue
return (static_cast<T&&>(arg));
}
template<class T>
constexpr T&& forward(std::remove_reference_t<T>&& arg) noexcept{
// forward an rvalue as an rvalue
return (static_cast<T&&>(arg));
}
std::remove_reference_t是一个模板类的类型别名,用于去掉T的引用属性(但不会去掉const属性,const属性可以用std::remove_const_t来去掉);如果forward接受的参数为左值的话,它将其转化成右值返回,这样既可以传给左值,又可以传给右值;如果传递的参数是个右值的话,它将其保留右值属性返回,这样只可以返回给右值。
五、remove_reference
remove_reference 是一个常用的类型转换工具模板,可以用于去除引用类型的引用修饰符
对于remove_reference是通过类模板的部分特例化进行实现的,其实现如下
// 普通版本
template <typename T>
struct remove_reference
{
typedef T type;
};
//部分版本特例化,将用于左值引用
template <typename T>
struct remove_reference<T&>
{
typedef T type;
};
//部分版本特例化,将用右值引用
template <typename T>
struct remove_reference<T&&>
{
typedef T type;
};
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a; //使用原版本,
remove_refrence<decltype(i)>::type b; //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type b; //右值引用特例版本
参考:
C++:浅谈右值引用_拥抱@的博客-CSDN博客_函数可以将一个变量转换为右值引用类型