C++ Rvalue Reference

C++ Rvalue Reference

Value categories

Value-categories

每个 C++ 表达式(带有操作数的操作符、字面量、变量名等)都属于三种基本值类别中的一种:

更具体的示例描述请查看 Value categories – cppreference.com

  • 纯右值 (prvalue)

    纯右值表达式没有可通过程序访问的地址。

    纯右值表达式的示例包括:(除了字符串字面量之外的)字面量、返回非引用类型的函数调用,以及在表达式评估期间创建但仅由编译器访问的临时对象

  • 亡值 (xvalue)

    亡值表达式的地址不能再由您的程序访问,但可用于初始化 rvalue 引用,后者提供对表达式的访问。

    亡值表达式的示例包括返回右值引用的函数调用,以及数组下标、成员和指针到数组或对象为右值引用的成员表达式

  • 左值 (lvalue)

    左值具有程序可以访问的地址。

    左值表达式的示例包括变量名称,包括常量变量、数组元素、返回 lvalue 引用、位字段、联合和类成员的函数调用

随着移动语义引入到 C++11 之中,值类别被重新进行了定义,以区别表达式的两种独立的性质:

  • 拥有身份 ( has identity):可以确定表达式是否与另一表达式指代同一实体,例如通过比较它们所标识的对象或函数的(直接或间接获得的)地址;
  • 可被移动:移动构造函数、移动赋值运算符或实现了移动语义的其他函数重载能够绑定于这个表达式。

C++11 中:

  • 拥有身份且不可被移动的表达式被称作*左值 (lvalue)*表达式;
  • 拥有身份且可被移动的表达式被称作*亡值 (xvalue)*表达式;
  • 不拥有身份且可被移动的表达式被称作*纯右值 (prvalue)*表达式;
  • 不拥有身份且不可被移动的表达式无法使用。

拥有身份的表达式被称作“泛左值 (glvalue) 表达式”。左值和亡值都是泛左值表达式。

可被移动的表达式被称作“右值 (rvalue) 表达式”。纯右值和亡值都是右值表达式。

value-categories

Lvalue Reference

c++11之前,C 中仅存在一种引用类型,所以简单称之为引用,而在c11中其被称为左值引用

L-value reference Can be initialized with Can modify
Modifiable l-values Yes Yes
Non-modifiable l-values No No
R-values No No

而对const对象的左值引用可以使用左值或右值进行初始化

L-value reference to const Can be initialized with Can modify
Modifiable l-values Yes No
Non-modifiable l-values Yes No
R-values Yes No

对const对象的左值引用非常有用,因为它使得可以将任何类型的参数(左值或右值)传递给函数,而无需复制该参数

Rvalue Reference

右值引用可将左值和右值区分开,可以帮助您不必要的内存分配和复制操作需求,从而提高应用程序的性能。

相比于左值引用,

R-value reference Can be initialized with Can modify
Modifiable l-values No No
Non-modifiable l-values No No
R-values Yes Yes
R-value reference to const Can be initialized with Can modify
Modifiable l-values No No
Non-modifiable l-values No No
R-values Yes No

Move Semantics

要实现移动语义,通常向类提供移动构造函数, 并可以选择移动赋值运算符 (运算符 =)。 其源是右值的复制和赋值操作随后会自动利用移动语义。 与默认复制构造函数不同,编译器不提供默认移动构造函数。

假设一个类X,其中包含一个/多个指向 创建和拷贝开销较大的对象(资源) 的指针,则无需取消引用该指针并复制其内容,移动构造(拷贝)函数只需复制指针的地址, 这就是移动构造函数带来的性能优势所在(只需要浅拷贝)。

(由于参数是非常量引用,因此它可以修改传递给它的对象,可能具有破坏性)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
X& X::operator=(const X & rhs)
{
//make a clone of what rhs's member pointer refers to
//destruct the resources that this member pointer refers to
//attach the clone
}

X& X::operator=(X && rhs)
{
//swap this->pointer and rhs.pointer
}

int main()
{
X a;
a = X();
}

因此,通过const lvalue 引用或 rvalue 引用参数重载函数,来编写区分不可修改对象 (lvalue) 和可修改临时值 (rvalue) 的代码。

Forcing Move Semantics

如果你想通过一个变量(lvalue)来调用移动构造(拷贝)函数,std::move()会将变量(lvalue)转换为右值,使其绑定到移动构造(拷贝)函数

std::move() does not move, it casts.

std::move()实际上是通过static_cast实现的

1
return static_cast<typename std::remove_reference<T>::type&&>(t);
1
2
3
4
5
6
7
8
9
10
11
12
13
template<class T> 
void swap(T& a, T& b)
{
T tmp(std::move(a));
a = std::move(b);
b = std::move(tmp);
}

int main()
{
X a, b;
swap(a, b);
}

因为已经将变量强制转换为右值,而接收右值的函数可能会对该变量产生破坏性作用,在此之后使用变量的内容可能会导致不确定的行为,因此在变量经过std::move()之后,需要销毁或重新分配。

Perfect forwarding

完美转发可减少对重载函数的需求,并有助于避免转发问题。 当编写一个泛型函数,该函数将引用作为其参数,并将这些参数传递给(或转发)到另一个函数时,可能会出现转发问题。 例如,如果泛型函数采用 const T& 类型的参数,则调用的函数无法修改该参数的值。 如果泛型函数采用 T& 类型的参数,则无法使用右值(如临时对象或非字符串字面量)来调用该函数。

通常,若要解决此问题,则必须提供为其每个参数采用 T&const T& 的重载版本的泛型函数。 因此,重载函数的数量将基于参数的数量呈指数方式增加。 利用右值引用,函数模板会推导出其模板自变量类型,然后使用引用折叠规则,函数可接受任意参数并将其转发给另一个函数,就像已直接调用其他函数一样。

引用折叠规则:

  • T& & -> T&
  • T& && -> T&
  • T&& & -> T&
  • T&& && -> T&&

使用示例:

1
2
3
4
5
template<typename T, typename Arg> 
shared_ptr<T> factory(Arg&& arg)
{
return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}

std::forward从概念上所实现:

1
2
3
if (is_lvalue_reference<T>::value)//Code producing an lvalue reference expects the object to remain valid.
return t;
return std::move(t);

References

Rvalue Reference Declarator: && - Microsoft Docs

Value Categories - Microsoft Docs

Value categories - cppreference.com

Rvalue references in Chromium