目录
一. 问题引入
二. 右值引用
1. lvalue/rvalue/prvalue/xvalue
1.1 表达式与对象的概念
1.2 左值与右值
2. moving semantics
2.1 显示绑定
2.2 Move constructors
2.3 Move assignment operator
2.4 实例分析
// TODO
Quiz
REF
本章简单介绍下move语义的“来龙去脉”。
一. 问题引入
如下,先来看看拷贝赋值的一个过程。
Q1:上述过程看起来,
(1) “已知foo()返回的对象是临时的,要析构销毁的” ;
(2) f新开辟的地址空间“addr NewB”与foo()指向的地址空间大小和内容都是一样的。
那么,让f析构掉自己原来的地址空间“addr B”,然后f直接指向foo()的地址空间,foo()置为null不就可以了吗?这样还可以减少开辟和销毁内存空间的操作。过程如下:
Q2:当然基于Q1,我们还可以把delete f的地址空间addr B的任务交给foo(),即跟foo()的析构过程绑定在一起。这就是移动语义(moving semantics)。过程如下:
到此,“移动语义”就被引入了。接下来的事情就是介绍C++是如何定义“移动语义”的。
二. 右值引用
为了实现移动语义,C++11引入了右值引用。
对于类型T,T&& 被称为对T的右值引用,T&被称为对T的左值引用。同样的移动语义(moving semantics)也有对应的移动构造和移动赋值,结构如下:
1. 移动构造
类名 ( 类名
&&
)2. 移动赋值
类名
&
类名:: operator=
( 类名&&
)同时,调用者可以使用std::move()库函数来实现移动语义的转换。
有了一个大概的概念,先来看看如下代码及运行结果。
#include <iostream>
using namespace std;
void foo(int &&x)
{
cout << "call && rvalue reference"<< endl;
}
void foo(int &x)
{
cout << "call & lvalue reference" << endl;
}
int main() {
int i = 1;
int &lv = i; // 左值引用
int &&rv = 2; // 右值引用
foo(1); // call && rvalue reference
foo(i); // call & lvalue reference
foo(lv); // all & lvalue reference
foo(rv); // call & lvalue reference
return 0;
}
运行结果及分析如下:
// 因为foo(1);中的'1'是字面量,字面量为纯右值,这里不能用纯右值去初始化一个左值引用,所以会调用void foo(int &&x){}
call && rvalue reference
// 因为foo(i);中的i是变量名,即是一个左值,所以会调用void foo(int &x){}
call & lvalue reference
// 因为foo(lv);中的lv是一个左值引用,所以会调用void foo(int &x){}
call & lvalue reference
// 虽然foo(rv);中的rv是一个右值引用,但是因为rv是一个变量名,所以也是一个左值,进而会调用void foo(int &x){} --从这里也可知道 右值引用其实也是一个左值
call & lvalue reference
接下来再来分别介绍下上面提到的4个概念:左值、右值、左值引用、右值引用。
1. lvalue/rvalue/prvalue/xvalue
【《C++ Primer》(5th)4.1 基础】
C++的表达式要不然是右值(rvalue,读作“are-value”),要不然就是左值(lvalue,读作“ell-value”)。
这两个名词是从C语言继承过来的,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能。在C++语言中,二者的区别就没那么简单了。一个左值表达式的求值结果是一个对象或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。
1.1 表达式与对象的概念
Q:什么叫表达式?
A:《C++ Primer》里是这样解释的:“表达式(expression)最小的计算单元。一个表达式包含一个或多个运算对象,通常还包含一个或多个运算符。表达式求值会产生一个结果。例如,假设i和j是int对象,则i+j是一个表达式,它产生两个int值的和”。简单来说,表达式就是运算符和运算对象组成的序列,能指明一个计算且能够产生结果或作用。例如,“1+2”是表达式,“std::count << 1”也是表达式,但是 “std::count << 1;”则是一个语句。基本表达式(Primary Expression)有如下几种:
- this
- literals (e.g. 2 or "Hello, world")
- id-expressions, including
- suitably declared unqualified identifiers (e.g. n or cout),
- suitably declared qualified identifiers (e.g. std::string::npos), and
- identifiers to be declared in declarators
- lambda-expressions
(since C++11)
- fold-expressions
(since C++17)
- requires-expressions
(since C++20)
这里顺带再补充下什么叫对象。
一般来讲,对象要有size,生命周期,类型,值等属性。如下实体就不是对象:引用,函数,枚举项,类型,类的非静态成员,模板,类或函数模板的特化,命名空间,形参包,和 this。
引用(reference)只是已有对象或函数的别名,编译器不必为其分配内存,因此也不存在引用数组,引用的引用,指向引用的指针等。
1.2 左值与右值
左值 (lvalue, left value),赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。
右值 (rvalue, right value),右边的值,是指表达式结束后就不再存在的临时对象。
而 C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。
纯右值 (prvalue, pure rvalue),纯粹的右值,要么是纯粹的字面量,例如
10
,true
; 要么是求值结果相当于字面量或匿名临时对象,例如1+2
。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。需要注意的是,字面量除了字符串字面量以外,均为纯右值。而字符串字面量是一个左值,类型为
const char
数组。
【《C++ Primer》(5th)4.1 基础】
到目前为止,已经有几种我们熟悉的运算符是要用到左值的。
· 赋值运算符需要一个(非常量)左值作为其左侧运算对象,得到的结果也仍然是一个左值。· 取地址符(参见2.3.2节,第47页)作用于一个左值运算对象,返回一个指向该运算对象的指针,这个指针是一个右值。· 内置解引用运算符、下标运算符(参见2.3.2节,第48页;参见3.5.2节,第104页)、迭代器解引用运算符、string和vector的下标运算符(参见3.4.1节,第95页;参见3.2.3节,第83页;参见3.3.3节,第91页)的求值结果都是左值。
· 内置类型和迭代器的递增递减运算符(参见1.4.1节,第11页;参见3.4.1节,第96页)作用于左值运算对象,其前置版本(本书之前章节所用的形式)所得的结果也是左值。
· 内置类型和迭代器的递增递减运算符(参见1.4.1节,第11页;参见3.4.1节,第96页)作用于左值运算对象,其前置版本(本书之前章节所用的形式)所得的结果也是左值。
上面讲了一大堆,其实能记住的也就是下面几句话:
1. 任何有名字的表达式都是左值(枚举例外);
2. 字面量除了字符串字面量以外,均为纯右值。而字符串字面量是一个左值,类型为 const char
数组。
3. 右值的关键字在”临时“,例如,
(1) 求值结果相当于字面量或匿名临时对象,例如 1+2就是右值
。
(2) 非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。
value category | 是否是临时的 | 是否占内存 | 是否能被取地址 | 是否能被赋值 | 是否能用来初始化引用 | ||
lvalue(left/locator value) | N | Y | Y | Y | Y (例如 double d = 2.0; double& rd = d;) | ||
rvalue | prvalue(pure right value) 纯右值 | Y | N | 例如 Foo f = f(); 这里的f()就是一个纯右值。 | N | N | Y (例如const int& r1 = 1; 1先被materialize,由prvalue转为xvalue) |
xvalue(eXpiring value) 将亡值 | Y | Y | 右值不必占内存,并不是说不能占内存。例如f()这个函数是右值也是临时的,但是它必须占内存,因为需要使用f().X访问成员。 | N | N | 仅能初始化const & |
因为lvalue和xvalue都占内存,因此有的地方也把lvalue和xvalue统称为glvalue(generalized lvalue,泛化的左值)。 const int& r1 = 1; 1由prvalue转为xvalue
2. moving semantics
2.1 显示绑定
int &&r1 = 1;// 正确
int &&r2 = r1; // 错误,因为r1是左值
需要改成如下
int&& r2 = std::move(r1); // 正确
用标准库函数std::move()来实现移动语义的自动转换。
2.2 Move constructors
Move constructors - cppreference.com
移动构造的语法规则如下:
可以使用标准库函数std::move()来实现自动转换。
// C++ reference 例子
#include <iomanip>
#include <iostream>
#include <string>
#include <utility>
struct A
{
std::string s;
int k;
A() : s("test"), k(-1) {}
A(const A& o) : s(o.s), k(o.k) { std::cout << "move failed!\n"; }
A(A&& o) noexcept :
s(std::move(o.s)), // explicit move of a member of class type
k(std::exchange(o.k, 0)) // explicit move of a member of non-class type
{}
};
A f(A a)
{
return a;
}
struct B : A
{
std::string s2;
int n;
// implicit move constructor B::(B&&)
// calls A's move constructor
// calls s2's move constructor
// and makes a bitwise copy of n
};
struct C : B
{
~C() {} // destructor prevents implicit move constructor C::(C&&)
};
struct D : B
{
D() {}
~D() {} // destructor would prevent implicit move constructor D::(D&&)
D(D&&) = default; // forces a move constructor anyway
};
int main()
{
std::cout << "Trying to move A\n";
A a1 = f(A()); // return by value move-constructs the target
// from the function parameter
std::cout << "Before move, a1.s = " << std::quoted(a1.s)
<< " a1.k = " << a1.k << '\n';
A a2 = std::move(a1); // move-constructs from xvalue
std::cout << "After move, a1.s = " << std::quoted(a1.s)
<< " a1.k = " << a1.k << '\n';
std::cout << "\nTrying to move B\n";
B b1;
std::cout << "Before move, b1.s = " << std::quoted(b1.s) << "\n";
B b2 = std::move(b1); // calls implicit move constructor
std::cout << "After move, b1.s = " << std::quoted(b1.s) << "\n";
std::cout << "\nTrying to move C\n";
C c1;
C c2 = std::move(c1); // calls copy constructor
std::cout << "\nTrying to move D\n";
D d1;
D d2 = std::move(d1);
}
运行结果如下
2.3 Move assignment operator
Move assignment operator - cppreference.com
可以使用标准库函数std::move()来实现自动转换。
#include <iostream>
#include <string>
#include <utility>
struct A
{
std::string s;
A() : s("test") {}
A(const A& o) : s(o.s) { std::cout << "move failed!\n"; }
A(A&& o) : s(std::move(o.s)) {}
A& operator=(const A& other)
{
s = other.s;
std::cout << "copy assigned\n";
return *this;
}
A& operator=(A&& other)
{
s = std::move(other.s);
std::cout << "move assigned\n";
return *this;
}
};
A f(A a) { return a; }
struct B : A
{
std::string s2;
int n;
// implicit move assignment operator B& B::operator=(B&&)
// calls A's move assignment operator
// calls s2's move assignment operator
// and makes a bitwise copy of n
};
struct C : B
{
~C() {} // destructor prevents implicit move assignment
};
struct D : B
{
D() {}
~D() {} // destructor would prevent implicit move assignment
D& operator=(D&&) = default; // force a move assignment anyway
};
int main()
{
A a1, a2;
std::cout << "Trying to move-assign A from rvalue temporary\n";
a1 = f(A()); // move-assignment from rvalue temporary
std::cout << "Trying to move-assign A from xvalue\n";
a2 = std::move(a1); // move-assignment from xvalue
std::cout << "\nTrying to move-assign B\n";
B b1, b2;
std::cout << "Before move, b1.s = \"" << b1.s << "\"\n";
b2 = std::move(b1); // calls implicit move assignment
std::cout << "After move, b1.s = \"" << b1.s << "\"\n";
std::cout << "\nTrying to move-assign C\n";
C c1, c2;
c2 = std::move(c1); // calls the copy assignment operator
std::cout << "\nTrying to move-assign D\n";
D d1, d2;
d2 = std::move(d1);
}
运行结果如下:
2.4 实例分析
https://gcc.godbolt.org/z/1q9qcK9Pb
#include <iostream>
#include <cstring>
#include <utility>
using namespace std;
class String {
char * content;
public:
String(const char * str = "") {
if (str) {
content = new char[strlen(str) + 1];
strcpy(content, str);
}
cout << (void*)content << " : ctor\n";
}
String(const String &s) {
content = new char[strlen(s.content) + 1];
strcpy(content, s.content);
cout << (void*)content << " : copy ctor\n";
}
String(String &&s) noexcept
: content(std::exchange(s.content, nullptr)) {
cout << (void*)content << " : move ctor\n";
cout << (bool)s.content << ", " << (void*)s.content << "\n";
}
String & operator=(const String &s) {
if (this == &s) return *this;
if (!content || strlen(content) != strlen(s.content)) {
delete[] content;
content = new char[strlen(s.content) + 1];
}
strcpy(content, s.content);
cout << (void*)content << " : copy assignment\n";
return *this;
}
String & operator=(String && s) noexcept {
std::swap(content, s.content);
cout << (void*)content << " : move assignment\n";
return *this;
}
~String() {
cout << (void*)content << " : dtor\n";
delete[] content;
}
};
class Msg {
String content;
unsigned from, to;
public:
explicit Msg(const char * content, unsigned from, unsigned to) :
content(content), from(from), to(to) {}
};
int main() {
Msg a("msg", 1, 2);
Msg b = a; // copy ctor
Msg c = std::move(a); // move ctor
c = b; // copy assign
c = std::move(b); // move assign
return 0;
}
运行结果
// TODO
Quiz
int& r = 1; // 错误,因为1是rvalue,是无法取地址的,因此这样写是非法的
const int& r = 1; // 正确,因为1是先由prvalue转为xvalue,xvalue是占内存的。
再分析下如下“1 = i;”错误的原因
int i;
i = 1; // 正确
1 = i; // 错误
REF
1. 《Modern C++ Tutorial: C++ 11/14/17/20 On the Fly》
2. 《C++ Primer》(5th)
3. C++ reference - cppreference.com