【c++工程实践】值语义与右值引用

1、值语义html

值语义(value sematics)指的是对象的拷贝与原对象无关,C++ 的内置类型(bool/int/double/char)都是值语义,标准库里的 complex<> 、pair<>、vector<>、map<>、string 等等类型也都是值语意,拷贝以后就与原对象脱离关系。对一个具备值语义的原始变量变量赋值能够转换成内存的bit-wise-copy数组

对于用户定义的类,若是一个type X 具备值语义, 则:安全

1)X 的size在编译时能够肯定。函数

2)将X的变量x,赋值与另外一个变量y,无须专门的 = operator,简单的bit-wise-copy 便可。spa

3)当上述赋值发生后,x和y脱离关系:x 和 y 能够独立销毁, 其内存也能够独立释放。指针

 1 了解第三点很重要,好比下面的class A就不具有值语义:
 2 class A
 3 {
 4      char * p;
 5      public:
 6           A() { p = new char[10]; }
 7           ~A() { delete [] p; }
 8 };
 9 A 知足1和2,但不知足3。由于下面的程序会出错误: 
10 Foo()
11 {
12     A a;
13     A b = a;
14 } // crash here

与值语义对应的是“对象语义/object sematics”,或者叫作引用语义(reference sematics),引用语义指的是面向对象意义下的对象,对象拷贝是禁止的。拷贝 TcpConnection 对象也没有意义,系统里边只有一个 TCP 链接,拷贝 TcpConnection  对象不会让咱们拥有两个链接。code

C++ 要求凡是能放入标准容器的类型必须具备值语义。准确地说:type 必须是 SGIAssignable concept 的 model。可是,由 于C++ 编译器会为 class 默认提供 copy constructor 和 assignment operator,所以除非明确禁止,不然 class 老是能够做为标准库的元素类型——尽管程序能够编译经过,可是隐藏了资源管理方面的 bug。所以,在写一个 class 的时候,先让它继承 boost::noncopyable,几乎老是正确的。htm

在现代 C++ 中,通常不须要本身编写 copy constructor 或 assignment operator,由于只要每一个数据成员都具备值语义的话,编译器自动生成的 member-wise copying&assigning 就能正常工做;若是以 smart ptr 为成员来持有其余对象,那么就能自动启用或禁用 copying&assigning对象

t2 = t1;  // calls assignment operator, same as "t2.operator=(t1);"
Test t3 = t1;  // calls copy constructor, same as "Test t3(t1);"

 值语义用于控制对象的生命期,而其具体的控制方式分为两种:blog

    • 生命期限于scope内:无需控制,到期自动调用析构函数。
    • 须要延长到scope外:移动语义。

由于右值引用的目的在于实现移动语义,因此右值引用 意义便是增强了值语义对对象生命期的控制能力。

2、左值、右值

左值对应变量的存储位置,而右值对应变量的值自己。左值与右值的根本区别在因而否容许取地址&运算符得到对应的内存地址.

C++03及之前的标准定义了在表达式中左值到右值的三类隐式自动转换:

  • 左值转化为右值;如整型变量i在表达式 (i+3)
  • 数组名是常量左值,在表达式[注 5]中转化为数组首元素的地址值
  • 函数名是常量左值,在表达式中转化为函数的地址值

不严格的来讲,左值对应变量的存储位置,而右值对应变量的值自己。

3、右值引用

3.1 左值引用

引用底层是用const指针实现的,分配额外的内存空间。

准确地说

1 int b=0;
2 int &a=b;

这种状况等同于

1 int b=0;
2 int *const lambda=&b;
3 //此后 *lambda就彻底等价于上面的a

经过简单的例子:

 1 int test_lvalue() {
 2   int b = 0;
 3   int& rb = b;
 4   rb = 1;
 5 
 6   return b;
 7 }
 8 
 9 int test_pointer() {
10   int b = 0;
11   int* pb = &b; 
12   *pb = 1;
13 
14   return b;
15 }

=> g++ -g -O0 test.cc

=> objdump -d -S a.out > test.i

 1 int test_pointer() {
 2   400876: 55                    push   %rbp
 3   400877: 48 89 e5              mov    %rsp,%rbp
 4   int b = 0;
 5   40087a: c7 45 f4 00 00 00 00  movl   $0x0,-0xc(%rbp)
 6   int* pb = &b; 
 7   400881: 48 8d 45 f4           lea    -0xc(%rbp),%rax
 8   400885: 48 89 45 f8           mov    %rax,-0x8(%rbp)
 9   *pb = 1;
10   400889: 48 8b 45 f8           mov    -0x8(%rbp),%rax
11   40088d: c7 00 01 00 00 00     movl   $0x1,(%rax)
12 
13   return b;
14   400893: 8b 45 f4              mov    -0xc(%rbp),%eax
15 }
16   400896: c9                    leaveq 
17   400897: c3                    retq  
 1 int test_lvalue() {
 2   400854: 55                    push   %rbp
 3   400855: 48 89 e5              mov    %rsp,%rbp
 4   int b = 0;
 5   400858: c7 45 f4 00 00 00 00  movl   $0x0,-0xc(%rbp)
 6   int& rb = b;
 7   40085f: 48 8d 45 f4           lea    -0xc(%rbp),%rax
 8   400863: 48 89 45 f8           mov    %rax,-0x8(%rbp)
 9   rb = 1;
10   400867: 48 8b 45 f8           mov    -0x8(%rbp),%rax
11   40086b: c7 00 01 00 00 00     movl   $0x1,(%rax)
12 
13   return b;
14   400871: 8b 45 f4              mov    -0xc(%rbp),%eax
15 }
16   400874: c9                    leaveq
17   400875: c3                    retq   

经过汇编指令,咱们也能够看到,左值引用和指针在汇编层面是一致的。

 

3.2 右值引用

C++中右值能够被赋值给左值或者绑定到引用。类的右值是一个临时对象,若是没有被绑定到引用,在表达式结束时就会被废弃。因而咱们能够在右值被废弃以前,移走它的资源进行废物利用,从而避免无心义的复制。被移走资源的右值在废弃时已经成为空壳,析构的开销也会下降。

右值中的数据能够被安全移走这一特性使得右值被用来表达移动语义。以同类型的右值构造对象时,须要以引用形式传入参数。右值引用顾名思义专门用来引用右值,左值引用和右值引用能够被分别重载,这样确保左值和右值分别调用到拷贝和移动的两种语义实现。对于左值,若是咱们明确放弃对其资源的全部权,则能够经过std::move()来将其转为右值引用。

std::move

std::move()其实是static_cast<T&&>()的简单封装。

1 template <typename T>
2 decltype(auto) move(T&& param)
3 {
4    using return_type = std::remove_reference<T>::type&&;
5    return static_cast<return_type>(param);
6 }

咱们能够看见这里面的逻辑实际上是不管你的param是何种类型,都会被强制转为右值引用类型。

这里须要注意的是模版这里的T&& 类型,我愿意欣赏与接受Meyers的叫法,他把这样的类型叫作Universal Reference。对于Universal Reference来讲,若你传递的param是一个左值,那么T将会被deduce成Lvalue Reference(左值引用),其Param Type也是左值引用。若你传递进来的param是右值,那么T则是正常的param类型,如int等,其Param Type结果是T&&。

举一个简单的栗子

1 template<typename T>
2 void foo(T&& param);
3 
4 int i = 7;
5 foo(i);
6 foo(47);

i是一个左值,因而T被deduce成int&,因而变为了

1 foo(int& &&);

而整个参数的结果类型,即Param Type为int&,C++不容许reference to reference,会进行引用折叠,其规则为:

1.当类型推导时可能会间接地建立引用的引用,此时必须进行引用折叠。具体折叠规则以下:

    A. X& &、X& &&和X&& &都折叠成类型X&。即凡有左值引用参与的状况下,最终的类型都会变成左值引用。

    B. 类型X&& &&折叠成X&&。即只有所有为右值引用的状况才会折叠为右值引用。

2.引用折叠规则暗示咱们,能够将任意类型的实参传递给T&&类型的函数模板参数。

而对于foo(47),因为47是右值,那么T被正常的deduce成int,因而变为了

1 foo(int &&);

 

std::forward

对于forward,其boost的实现基本能够等价于这样的形式:

1 template <typename T>
2 T&& forward(typename remove_reference<T>::type& param)
3 {
4     return static_cast<T&&>(param);
5 }

那么这里面是如何达到完美转发的呢?

举一个栗子

1 template<typename T>
2 void foo(T&& fparam)
3 {
4     std::forward<T>(fparam);
5 }
6 
7 int i = 7;
8 foo(i);
9 foo(47);

如上文所述,这里的i是一个左值,因而,咱们在void foo(T&& fparam)这里的话,T将会被deduce成int& 而后Param Type为int&。(注意,我这里使用的变量名字为fparam,以便与forward的param进行区分)

那么为何Param Type会是int&呢?由于按照正常的deduce,咱们将会获得

1 void foo(int& &&fparam);

先前我简单的提到了一句,C++不容许reference to reference,然而事实上,咱们却会出现

Lvalue reference to Rvalue reference[1],

Lvalue reference to Lvalue reference[2],

Rvalue reference to Lvalue reference[3],

Rvalue reference to Rvalue reference[4]

等四种状况,那么针对这样的状况,编译器将会根据引用折叠规则变为一个Single Reference,那么是左值引用仍是右值引用呢?其实这个规则很简单,只要有一个是左值引用,那么结果就是左值引用,其他的就是右值引用。因而咱们知道了[1][2][3]的结果都是左值引用,只有[4]会是右值引用,而要从

1 void foo(T&& fparam)

这里T的Universal Reference让fparam拥有右值引用类型,那么则须要保证传递归来的参数为右值才能够,由于如果左值的话,T会deduce成左值引用,结合引用折叠规则,fparam的类型会是左值引用类型。

因而咱们如今来看,int& &&这样的状况属于Lvalue reference to Rvalue reference,结果则为左值引用。那么,咱们这个时候带入到forward函数来看看,首先是T变为了int&,通过了remove_reference变为了int,结合后面跟上的&,则变为了int&。而后咱们再次替换 static_cast和return type的T为int&,都获得了int& &&

1 int& && forward(int& param)
2 {
3     return static_cast<int& &&>(param);
4 }

因而再应用引用折叠规则,int& &&都划归为了int&

1 int& forward(int& param)
2 {
3     return static_cast<int&>(param);
4 }

因而,咱们能够发现咱们fparam变量的左值引用类型被保留了下来。这里也须要注意,咱们到达forward的时候就已是左值引用了,因此forward并无改变什么。

如咱们这时候是47这样的右值,咱们知道了T会被deduce成int,通过了remove_reference,变为了int,跟上后面的&,成为了int&,而后再次替换static_cast和返回类型的T为int&&

1 int && forward(int& param)
2 {
3     return static_cast<int&&)(param);
4 }

因而,咱们也能够发现,咱们fparam变量的右值引用类型也完美的保留了下来。

相关文章
相关标签/搜索