C++ 核心概念¶
Abstract
jyy os
右值引用:Rvalue References¶
Rvalue 和 Lvalue¶
为什么需要右值引用,其引入解决了两个问题: - 移动语义: - 完美转发:
首先需要区别什么是 lvalues
以及 rvalues
,这两个概念最开始是从C中引入的,但是C++有些不同,但是需要注意:
-
lvalue & rvalue
是expression
的属性,即value category
,表达式是运算符和操作数的序列,用来指明计算,计算的结果是产生值,也可以导致副作用,比如:std::cout << 1
-
primary expression:
-
为什么
int m; m + 1 = 2;int foo(); foo() = 2
这些表达式是违法的,其实每个表达式其实都有两个性质,一个是我们熟知的 类型(type)
,而另一个就是我们刚刚发现的「新区别」:value category
。
lvalue¶
An lvalue is an expression
that refers to a memory location
and allows us to take the address of that memory location via the &
operator.
- 任何有名字
的表达式都是左值,比如int n = 1
,唯一的例外是枚举
-
内置 的
++a
,a = b
,a += b
, p, p->m 是 lvalue,因为a = b
返回的是a的引用,p和p->m体现占内存的操作。 -
返回值为引用类型的函数调用表达式是 lvalue,因为返回值表征的是一个对象,比如
int& foo()
-
如果 a 是
lvalue
,m 是成员变量,则a.m
是lvalue
;如果 m 是成员变量,p->m
是 lvalue。 -
除了字符串字面量是 lvalue(因为字符串字面量的类型是
const char[N]lex.string#5
以外,其他字面量都是 rvalue
rvalue:¶
右值不表征对象,非左值表达式就是右值表达式,右值常用来完成以下两件事:
-
计算内置操作符(非重载运算符)的一个操作数,比如
1 + 2 + 3
,即1 + 2
是rvalue,作为第二个+
的操作数。 -
初始化对象
- 初始化出来的这个对象称为这个表达式的
result object
int i = 1 + 1; void f(int x); f(1)
,用来初始化i和参数x
- 初始化出来的这个对象称为这个表达式的
右值包括:
- 枚举数 (enumerator) 和除了字符串字面量以外的字面量是 rvalue
,比如int i = 100
,100是右值
-
a++
,因为a++返回的是原来的a,a + b
,a || b
,a < b
,&a
等表达式是 rvalue -
目标为非引用类型的 cast expression 是 rvalue,如 int(3.0)
-
this 是 rvalue,因为this不能赋值
-
lambda 表达式是 rvalue
Move Semantics¶
假设类X拥有某些资源的pointer或者handler
,所以当类X发生拷贝的时候,需要自定义拷贝构造函数。
拷贝赋值可能发生的情况
对于x = foo()
这一语句,发生以下事情:
- clones the resource from the
temporary
returned by foo destructs
the resource held by x and replaces it with the clonedestructs
thetemporary
and thereby releases its resource
用图表示为:
很明显,对于这种声明周期快要结束的temporary object
而言重复这种拷贝和析构是低效的,能否提供一种机制让x
直接交换
temporary object的资源,然后让temporary object
在析构的时候将x原来的资源释放掉。
即我们需要一个特殊的copy assignment operator
,在等式右边是右值的时候,做到:
move semantics
,考虑如何实现函数重载,对于overload of the copy assignment operator
,这个mystery type
需要有以下特点:
- 必须是reference
- 如果传入rvalue则选择
mystery type
,lvalue则选择ordinary reference
reference
即Rvalue Reference
,效果如下:
Forcing Move Semantics(std::move)¶
C++设计右值引用最开始是为了实现move semantics
以提高性能,但是如果程序员同样需要在lvalue
上实现move semantics
,C++也提供了对应的机制,比如下面这个模板函数:
这里并没有右值,但是这里的swap
如果使用移动语义,性能更好,省区拷贝析构的代价,所以C++11提供了一个std::move()
,它将其参数转换为右值,而不执行任何其他操作,即程序员如果意思到当前的对象可能发生资源转换,可以通过std::move
将其转换为右值,使用过std::move
后的object里面资源状态是未确定的,Moved-from objects exist in an unspecified, but valid
, state,即做下面事情是合法的
- destruction
- assignment
- const observers such as get, empty, size
使用std::move
后的swap
函数如下:
注意如果我们写下a = std::move(b)
,我们希望a
和b
的资源发生交换,但是如果仅仅是发生交换,被交换后a拥有的资源何时被析构暂时没做定义,所以需要在交换后要保证b
处于可以被析构以及赋值的状态。
Is an Rvalue Reference an Rvalue¶
考虑下面这个函数,假设X
已经实现移动语义。
一般如果考虑传入的是右值引用,那么下面的代码应该调用X(X&& rhs)
,但是正确的判断的准则为:
if it has a name, then it is an lvalue. Otherwise, it is an rvalue.
移动语义学的全部意义在于只在“无关紧要”的地方应用它,即我们移动的对象在移动之后立即死亡和消失。因此有了这样的规则,比如:
-
对于
x = foo()
是在「不影响结果的地方」使用,因为这样一定不会带来问题 -
对于
std::move
,则是「程序员明确知道这里使用移动不影响结果」,程序员对这样的移动语义负责
但是对于X anotherX = x;
,由于后续的操作可能使用x,并且仍在作用域中,所以窃取是危险的行为。
应用if-it-has-a-name rule
可以有以下判断,goo()
的返回值没有名字,但是我们通过右值引用延长了其声明周期,所以为xvalue
。
上面的这一点要在继承类的构造函数中特别注意,比如Base
类实现了移动语义,Derived
继承了Base
,对于Derived
的构造函数:
Move Semantics and Compiler Optimizations¶
考虑下面的代码片段:如果禁用Return Value Optimization
,可能会天真的以为这个函数会调用copy constructor
来用x
初始化返回值临时对象,然后局部变量x会被析构,既然已知x的生命周期,能够采用move-constructor
来构造返回值临时对象
constructing an X locally
然后copying it out
, 现代编译器通常采用会construct the X object directly at the location of foo's return value
,即称为return value optimization
,这优于move semantics
Perfect Forwarding¶
设计move semantics
的目的之一是为了解决perfect forwarding problem
,那到底什么是完美转发:
- 完美转发:接收任意数量实参的
函数模板
,它可以将实参转发到其他
的函数,使目标函数接收到的实参与被传递给转发函数的实参保持一致
比如考虑下面的函数模板:想让factory function
的arg转发到 T's constructor
.
call by value
,这导致采用引用为参数的构造函数失败,是否对ParamType
变为引用即解决问题呢,即
这个错误可以通过修改为const解决:但是如果factory
函数参数的个数变多,这个方法变得不可扩展,你需要提供各种const&non-const
变体,而且这种方法这种方法没法实现移动语义,因为这里全是左值,但是右值引用可以解决这个问题。
reference collapsing
的规则:这个规则还可以应用在模板的ParamType
的推导上,
- T& &
becomes T&
- T& &&
becomes T&
- T&& &
becomes T&
- T&& &&
becomes T&&
对于ParamType
是一个通用引用的情景,规则是:
- 如果
expr
是右值,忽略引用,比如对于右值A
,T
被推导为A
,ParamType
类型即为A&&
- 如果
expr
是左值,T
和ParamType
都会被推导为左值引用,比如对于左值A
,T被推导为T&
,然后T& && -> T&,ParamType
类型也为A&
考虑上面的规则,之前的函数形式修改为:
arg
是万能引用,对通用引用使用std::forward
。
这里的std::forward
的形式如下: