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,允许出现引用的引用。这些引用会按照一定的规则最终折叠起来:
- 右值引用的右值引用折叠为右值引用
- 其他所有类型折叠为左值引用
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&&
万能引用:万能引用又被叫做转发引用,他既可能是左值引用,又可能是右值引用。 当满足以下两种情况时,此时属于万能引用:
- 函数参数是T&&, 且T是这个函数模板的模板类型
template<class T>
int f(T&& x) // x is a forwarding reference
{
}
- 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: