C++04_左值右值、move语义、万能引用、引用折叠、forward

左值、右值、左值引用、右值引用

左值(lvalue)

左值不能简单理解为就是等号左边的值,其实只要能取地址,那这个表达式就是左值。可以取地址意味着在程序的某块内存地址上已经存储了他的内容。

举例一些常见的左值:

  • 具名的变量名
  • 左值引用
  • 右值引用也是左值
  • 返回左值引用的函数或是操作符重载的调用语句。
  • a=b, a+=b, 等内置的赋值表达式。
  • 前缀自增。如 ++a, --a 是左值。
  • 字符串常量
  • 左值引用的类型转换语句。如 static_cast<int&>(x)
int a = 1;
const char* str = "hello";

这里 a 是左值,因为 a 这个变量确实被存到内存里了,并且在内存里面写入的值是 1。 同理,str 也是左值。

1 是在运行到这行代码是,临时产生的一个值,他是没有地址的, 仅仅存在寄存器中用作临时运算。所以数字常量 1 不是左值。

但是需要注意的是,"hello" 这个字符串常量实实在在的是左值,原因如下:编译的时候, hello 这个字符串会真的被单独的存放在某一内存地址上存储,一般是静态数据区。所以你直接对 hello 这个字符串常量 取地址(&),是完全可以取到的。能取到地址说明他就是个左值。

所以,现在可以解答以下问题了:

左值一定能赋值?不是, 字符串常量是左值,但不能修改其值。

左值一定能取地址? 是的。

右值(rvalue)

右值是临时产生的值,不能对右值取地址,因为它本身就没存在内存地址空间上。

举例:

  • 除字符串以外的常量,如 1,true,nullptr
  • 返回非引用的函数或操作符重载的调用语句。
  • a++, a--是右值
  • a+b, a << b 等
  • &a,对变量取地址的表达式是右值。
  • this指针
  • lambda表达式
  • 将亡值:
    • 返回右值引用的函数或者操作符重载的调用表达式。如某个函数返回值是 std::move(x), 并且函数返回类型是 T&&
    • 目标为右值引用的类型转换表达式。如 static<int&&>(a)

左值引用

左值引用可以分为两种:非const左值引用和 const左值引用

有很重要的一点是,非const左值引用只能绑定左值;const左值引用既能绑定左值,又能绑定右值!

右值引用

简单说,右值引用就是有两个&&的变量:T&&。右值引用只能绑定到右值上。

move语义

move唯一做的事情就是类型转换,std::move(x)它完全等价于static_cast<T&&>(x)。

move 并不作任何的资源转移操作。单纯的 move(x) 不会有任何的性能提升,不会有任何的资源转移。它的作用仅仅是产生一个标识x的右值表达式。而经过 move 之后,就能用右值引用将其绑定。

int b = 2;
// int&& rref_b = b; // error,右值引用只能绑定到右值上,b是一个左值
int&& rref_b = std::move(b); // ok, std::move(b) 是一个右值,可以用右值引用绑定

什么时候应该实现移动构造函数?

string 的拷贝构造函数和移动构造函数:

// 拷贝构造函数
string(const string& b) {
  m_length = b.m_length;
  m_ptr = malloc(m_length);
  memcpy(b.m_ptr, m_ptr, b.length);
}

// 移动构造函数
string(string&& b) {
  m_length = b.m_length;
  m_ptr = b.m_ptr;
  b.m_ptr = nullptr;
}

其实只要是栈上的资源,都是采用复制的方式。而只有堆上的资源,才能复用旧的对象的资源。

为什么栈上的资源不能复用,而要重新复制一份?因为你不知道旧的对象何时析构,旧的对象一旦析构,其栈上所占用的资源也会完全被销毁掉,新的对象如果复用的这些资源就会产生崩溃。

为什么堆上的资源可以复用,因为堆上的资源不会自动释放,除非你手动去释放资源。

所以说,只有当你的类申请到了堆上的内存资源的时候,才需要专门实现移动构造函数,否则其实没有必要,因为他的消耗跟拷贝构造函数是一模一样的。

STL 标准库的容器基本都提供了右值引用的重载。

引用折叠、万能引用与完美转发

利用 模板 或 typedef,允许出现引用的引用。这些引用会按照一定的规则最终折叠起来:

  1. 右值引用的右值引用折叠为右值引用
  2. 其他所有类型折叠为左值引用
typedef int&  lref;
typedef int&& rref;
int n;

lref&  r1 = n; // type of r1 is int&
lref&& r2 = n; // type of r2 is int&
rref&  r3 = n; // type of r3 is int&
rref&& r4 = 1; // type of r4 is int&&

万能引用:万能引用又被叫做转发引用他既可能是左值引用,又可能是右值引用。 当满足以下两种情况时,此时属于万能引用:

  1. 函数参数是T&&, 且T是这个函数模板的模板类型
template<class T>
int f(T&& x)                      // x is a forwarding reference
{
}
  1. auto&&,并且不能是由初始化列表推断出来。
auto&& vec = foo();

为什么说他是万能引用,是因为它同时支持左值和右值入参。当我们入参传入左值时,他就是个左值引用;当我们入参传入右值时,他就是个右值引用

template<class T>
int f(T&& x) {
  push(x);
}

由于这里是万能引用,传进来的入参有可能是个左值,有可能是一个右值。然而形参 x 一定是一个左值,因为他是个具名的对象。直接 push(x) 的话,就相当于入参传递的一定是左值了。

也就是说,不论我们实际入参是左值还是右值,最后都会被当做左值来转发。即我们丢失了它本身的值类型。有没有办法能仍然保留其值属性?左值就按照左值转发,右值按照右值转发?

有的,完美转发 std::forward 就派上用场了。它的定义如下:

template< class T >
T&& forward( typename std::remove_reference<T>::type& t ) noexcept;

注意观察 std::forward 的返回值是 T&&。

在转发时,只需要这样做就行了:

template<class T>
int f(T&& x) {
  push(std::forward<T>(x));
}

Ref:

posted @ 2022-08-01 10:32  吹不散的流云  阅读(116)  评论(0编辑  收藏  举报