目录
一、左值和右值
二、左值引用和右值引用
三、移动语义
3.1 - 左值引用的短板
3.2 - 移动构造和移动赋值
四、move 函数
4.1 - remove_reference 的实现
4.2 - move 的实现
五、完美转发
5.1 - forward 的实现
5.2 - 使用场景
很多初学者都感觉右值引用晦涩难懂,其实不然,右值引用只不过是一种新的 C++ 语法,真正理解起来有难度的是基于右值引用引申出来的 2 种 C++ 编程技巧,分别为移动语义和完美转法。
一、左值和右值
C++ 中左值和右值的概念是从 C 语言中继承过来的,而我在学习 C 语言的过程中,也接触到了左值和右值,并通过下面的一道练习题对它们的概念做了详细的了解。
《C 陷阱与缺陷》练习 1-4:a+++++b
的含义是什么?
上式唯一有意义的分解方式是 a ++ + ++ b
,但根据"大嘴法"规则,上式应该被分解为 a ++ ++ + b
,不过这个式子从语法上来说是不正确的,它等价于 ((a++)++) + b
,其中 a++ 的结果不能作为左值,编译器不会接受 a++ 作为后面的 ++ 操作符的操作数。
术语"符号"(token)指的是程序中的一个基本组成单元,其作用相当于一个句子中的单词。
将程序分解成一个一个符号的过程,称为"词法分析",编译器中处理这一过程的部分,一般称为"词法分析器"。
词法分析中的"贪心法":
某些符号,例如 /、* 和 =,只有一个字符,称为单字符符号;而其他符号,例如 /*、== 以及标识符,包括了多个字符,称为多字符符号。当编译器读入一个字符 / 后又跟了一个字符 *,那么编译器必须做出判断,是将其作为两个分别的符号对待,还是合起来作为一个符号对待。解决方案可以归纳为一个很简单的规则:每一个符号应该包含尽可能多的字符,也就是说,编译器将程序分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是下一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串不再可能组成一个有意义的符号。这个处理策略有时被称为"贪心法",或者更口语化一点,被称为"大嘴法"。
需要注意的是,除了字符串与字符常量,符号的中间不能嵌有空白(空格符、制表符和换行符)。
所以,如果 / 是为了判断下一个符号而读入的第一个字符,而 / 之后紧接着 *,那么无论上下文如何,这两个字符都将被当成一个符号 /*,表示一段注释的开始。
y = x/*p;
上面语句的本意是用 x 除以 p 所指向的值,把所得的商再赋给 y,但实际上,/* 被编译器理解为一段注释的开始,编译器将不断地读入字符,直到 */ 出现。将上面的语句重写:
y = x/ *p;
或者更清楚一点,写作:
y = x/(*p);
在 C 语言或者 C++ 中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景的不同,分为左值表达式和右值表达式。通常情况下,判断某个表达式是左值还是右值,最常用的有以下 2 种方法:
-
左值既能够出现在等号左边,也能出现在等号右边;右值则只能出现在等号右边。
int a = 10; // 变量 a 是一个左值,而字面量 5 是一个右值 int b = a; // a、b 都是左值,只不过可以将 a 当做右值使用
-
有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
// &a++; // error &++a; // ok
a++ 是先复制一份临时的数据出来参与周围环境的运算,再让 a 自加 1,临时的数据没有固定的地址,所以 a++ 无法作为左值。
++a 则是先让 a 自加 1,再将其放到周围环境中参与运算,所以 a++ 可以作为左值。
注意:左值的英文简写为 "lvalue",右值的英文简写为 "rvalue",很多人可能会认为它们分别是 "left value" 和 "right value" 的缩写,其实不然,"lvalue" 是 "locator value" 的简写,指的是存储在内存中、有明确存储地址(可寻址)的数据,而 "rvalue" 是 "read value" 的简写,指的是可以提供数据值的数据(不一定可以寻址,例如存储在寄存器中的数据)。
二、左值引用和右值引用
C++98/03 标准中就有引用,使用 "&" 表示。但这种引用方式有一个缺陷,即只能引用左值,无法引用右值,因此 C++98/03 标准中的引用又称为左值引用。
int a = 10;
int& ra = a; // ok
// int& r = 10; // error
虽然 C++98/03 标准不支持为右值建立左值引用,但允许使用 const 左值引用来引用右值,也就是说,const 左值引用既可以引用左值,也可以引用右值。
int a = 10;
const int& ra = a; // ok
const int& r = 10; // ok
我们知道,右值往往是没有名称的,因此要使用它只能借助引用的方式,这样就产生了一个问题,实际开发中我们可能对右值进行修改(实现移动语义时就需要),显然 const 左值引用的方式是行不通的。
为此,C++11 标准引入了另一种新的引用方式,称为右值引用,用 "&&" 表示。
注意:和左值引用一样,右值引用也必须进行初始化,且只能使用右值进行初始化。
#include <iostream>
using namespace std;
int main()
{
// int a = 10;
// int&& rra = a; // error(不能使用左值初始化右值引用)
int&& rr = 10; // 使用右值进行初始化
rr = 100;
cout << rr << endl; // 100
return 0;
}
最后,C++ 语法上是支持 const 右值引用的,例如:
const int&& a = 10;
但这种定义出来的右值引用并无实际用处,一方面,右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;其次,const 右值引用的作用就是引用一个不可修改的右值,这项工作完全可以交给 const 左值引用完成。
三、移动语义
3.1 - 左值引用的短板
左值引用的使用场景是做函数的参数以及返回值,这样可以避免拷贝对象,提高效率,但当函数的返回值是一个局部对象(出了函数作用域就会被销毁),就不能使用左值引用返回,只能按值返回。
string.h:
#pragma once
#include <cassert>
#include <cstring>
#include <iostream>
namespace yzz
{
class string
{
public:
string(const char* str = "")
{
assert(str); // 前提是 str 非空
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
memcpy(_str, str, _size + 1);
}
// 拷贝构造函数(实现深拷贝)
string(const string& s)
: _size(s._size)
, _capacity(s._capacity)
, _str(new char[s._capacity + 1])
{
std::cout << "string(const string& s)" << std::endl;
memcpy(_str, s._str, _size + 1);
}
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
// 赋值运算符重载(利用上面写好的拷贝构造函数实现深拷贝)
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string& operator=(const string& s)
{
std::cout << "string& operator=(const string& s)" << std::endl;
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
test.cpp:
#include "string.h"
yzz::string func()
{
yzz::string s("hello world");
return s;
}
int main()
{
yzz::string ret1 = func();
// string(const string& s)
cout << "----------------------------" << endl;
yzz::string ret2;
ret2 = func();
// string(const string& s)
// string& operator=(const string & s)
// string(const string & s)
return 0;
}
3.2 - 移动构造和移动赋值
在 yzz::string 类中添加移动构造函数和移动赋值函数后,就可以解决函数按值返回时效率不高的问题。
// 移动构造
string(string&& rr)
: _str(nullptr), _size(0), _capacity(0)
{
std::cout << "string(string&& rr)" << std::endl;
swap(rr);
}
// 移动赋值
string& operator=(string&& rr)
{
std::cout << "string& operator=(string&& rr)" << std::endl;
swap(rr);
return *this;
}
C++11 标准对右值做了更加细致的划分,分别称为纯右值(Pure value,简称 pvalue)和将亡值(eXpiring value,简称 xvalue)。其中纯右值指的是内置类型的右值,而将亡值则指的是自定义类型的右值。
注意:
-
string(const string& s)
和string(string&& rr)
构成函数重载,且不会出现调用不明确的情况。string& operator=(const string& s)
和string& operator=(string&& rr)
同理。例如:#include <iostream> using namespace std; void func(const int& r) { cout << "void func(const int& r)" << endl; } void func(int&& rr) { cout << "void func(int&& rr)" << endl; } int main() { int a = 10; func(a); // void func(const int& r) func(10); // void func(int&& rr) return 0; }
所以在 yzz::string 类中添加移动构造函数和移动赋值函数后,使用该类的右值对象初始化同类对象或为同类对象赋值时,编译器会调用移动构造函数
string(string&& rr)
和移动赋值函数string& operator=(string&& rr)
。 -
无论是左值引用,还是右值引用,它们本身都是左值。
#include <iostream> using namespace std; void test(int& r) { cout << "左值引用" << endl; } void test(int&& r) { cout << "右值引用" << endl; } int main() { int a = 10; int& ra = a; cout << &ra << endl; // ok int&& rr = 10; cout << &rr << endl; // ok test(ra); // 左值引用 test(rr); // 左值引用 return 0; }
所以在移动构造函数和移动赋值函数中调用 swap 函数(参数类型为左值引用)是没问题的。
四、move 函数
move 本意为 "移动",但该函数并不能移动任何数据,仅仅只执行强制类型转换,无条件地将实参强制转换为右值。
在理解 move 是如何实现之前,需先理解 remove_reference 是如何实现的。
4.1 - remove_reference 的实现
template<class T>
struct remove_reference
{
typedef T type;
};
// 特化版本(偏特化)
template<class T>
struct remove_reference<T&>
{
typedef T type;
};
template<class T>
struct remove_reference<T&&>
{
typedef T type;
};
remove_reference 的作用是去除 T 中的引用部分,只获取其中的类型部分。
示例:
#include <type_traits>
#include <iostream>
int main()
{
typedef std::remove_reference<int>::type A;
typedef std::remove_reference<int&>::type B;
typedef std::remove_reference<int&&>::type C;
std::cout << std::is_same<int, A>::value << std::endl; // 1
std::cout << std::is_same<int, B>::value << std::endl; // 1
std::cout << std::is_same<int, C>::value << std::endl; // 1
return 0;
}
4.2 - move 的实现
万能引用(Universal Reference):如果模板(包括类模板和函数模板)中的函数参数写成 T&&,那么函数既可以接受右值,也能接受左值。
#include <iostream>
using namespace std;
template<class T>
void func(T&& arg)
{
cout << std::is_same<int, T>::value << " ";
cout << std::is_same<int&, T>::value << " ";
cout << std::is_same<int&&, T>::value << endl;
cout << std::is_same<int, T&&>::value << " ";
cout << std::is_same<int&, T&&>::value << " ";
cout << std::is_same<int&&, T&&>::value << endl;
}
int main()
{
func(10);
// 1 0 0
// 0 0 1
cout << "--------" << endl;
int a = 10;
func(a);
// 0 1 0
// 0 1 0
return 0;
}
注意:C++ 不允许对引用再进行引用,所有的折叠引用最终都代表一个引用,要么是左值引用,要么是右值引用。
引用折叠(Refence Collapse)的规则是:如果任一引用为左值引用,则结果为左值引用;否则,即两个都是右值引用,结果才是右值引用。总结:
T& & 变成 T&;
T& && 变成 T&;
T&& & 变成 T&;
T&& && 变成 T&&。
move 的实现:
template<class T>
typename remove_reference<T>::type&& move(T&& arg) noexcept
{
return static_cast<typename remove_reference<T>::type&&>(arg);
}
示例:
#include "string.h"
#include <utility>
int main()
{
yzz::string s1("hello world");
yzz::string s2 = std::move(s1);
return 0;
}
五、完美转发
5.1 - forward 的实现
转发左值:
template<class T>
T&& forward(typename remove_reference<T>::type& arg) noexcept
{
return static_cast<T&&>(arg);
}
T&& 可能会发生引用折叠,当 T 被推导为左值引用,则 T& && 为 T&;当 T 被推导为右值引用,则 T&& && 为 T&&。
转发右值:
template<class T>
T&& forward(typename remove_reference<T>::type&& arg) noexcept
{
return static_cast<T&&>(arg);
}
示例:
#include <utility>
#include <iostream>
using namespace std;
void Func(int& x) { cout << "左值引用" << endl; }
void Func(const int& x) { cout << "const 左值引用" << endl; }
void Func(int&& x) { cout << "右值引用" << endl; }
void Func(const int&& x) { cout << "const 右值引用" << endl; }
template<class T>
void PerfectForward(T&& t)
{
// Func(t); // (1) 注意:无论是左值引用,还是右值引用,它们都是左值
Func(std::forward<T>(t)); // (2) 完美转发
}
int main()
{
PerfectForward(10);
// 1、左值引用
// 2、右值引用
int a;
PerfectForward(a);
// 1、左值引用
// 2、左值引用
PerfectForward(std::move(a));
// 1、左值引用
// 2、右值引用
const int b = 8;
PerfectForward(b);
// 1、const 左值引用
// 2、const 左值引用
PerfectForward(std::move(b));
// 1、const 左值引用
// 2、const 右值引用
return 0;
}
5.2 - 使用场景
C++11 中,STL 容器的插入接口添加了右值引用的版本,以 list 容器的 push_back 接口为例:
void push_back(const value_type& val);
void push_back(value_type&& val);
list.h:
#pragma once
#include <utility>
namespace yzz
{
template<class T>
struct list_node
{
list_node<T>* _prev;
list_node<T>* _next;
T _data;
list_node(const T& data = T())
: _prev(nullptr), _next(nullptr), _data(data)
{ }
// 使用完美转发调用移动构造函数
list_node(T&& rr)
: _prev(nullptr), _next(nullptr), _data(std::forward<T>(rr))
{ }
};
template<class T>
class list
{
typedef list_node<T> Node;
void empty_initialize()
{
_phead = new Node;
_phead->_prev = _phead;
_phead->_next = _phead;
}
public:
list() { empty_initialize(); }
void push_back(const T& data)
{
Node* newnode = new Node(data);
newnode->_prev = _phead->_prev;
newnode->_next = _phead;
_phead->_prev->_next = newnode;
_phead->_prev = newnode;
}
void push_back(T&& rr)
{
// 使用完美转发调用移动构造函数
Node* newnode = new Node(std::forward<T>(rr));
newnode->_prev = _phead->_prev;
newnode->_next = _phead;
_phead->_prev->_next = newnode;
_phead->_prev = newnode;
}
private:
Node* _phead;
};
}
test.cpp:
#include "string.h"
#include "list.h"
int main()
{
yzz::list<yzz::string> lt;
// string(const string& s)
cout << "-----------------------------" << endl;
yzz::string s1("11111");
lt.push_back(s1);
// string(const string& s)
cout << "-----------------------------" << endl;
lt.push_back("22222");
// string(string&& rr)
return 0;
}
图解: