右值引用与转移语义

https://www.ibm.com/developerworks/cn/aix/library/1307_lisl_c11/

新特性的目的

右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding)。它的主要目的有两个方面:

  1. 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
  2. 能够更简洁明确地定义泛型函数。

左值与右值的定义

C++( 包括 C) 中所有的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,那些可以在多条语句中使用的对象。所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。右值是指临时的对象,它们只在当前的语句中有效。请看下列示例 :

  1. 简单的赋值语句
    1
    如:int i = 0;

    在这条语句中,i 是左值,0 是临时值,就是右值。在下面的代码中,i 可以被引用,0 就不可以了。立即数都是右值。

  2. 右值也可以出现在赋值表达式的左边,但是不能作为赋值的对象,因为右值只在当前语句有效,赋值没有意义。

    如:((i>0) ? i : j) = 1;

    在这个例子中,0 作为右值出现在了”=”的左边。但是赋值对象是 i 或者 j,都是左值。

    在 C++11 之前,右值是不能被引用的,最大限度就是用常量引用绑定一个右值,如 :

    1
    const int &a = 1;

    在这种情况下,右值不能被修改的。但是实际上右值是可以被修改的,如 :

    1
    T().set().get();

    T 是一个类,set 是一个函数为 T 中的一个变量赋值,get 用来取出这个变量的值。在这句中,T() 生成一个临时对象,就是右值,set() 修改了变量的值,也就修改了这个右值。

    既然右值可以被修改,那么就可以实现右值引用。右值引用能够方便地解决实际工程中的问题,实现非常有吸引力的解决方案。

左值和右值的语法符号

左值的声明符号为”&”, 为了和左值区分,右值的声明符号为”&&”。

示例程序 :

1
2
3
4
5
6
7
8
9
10
11
12
13
void process_value(int& i) {
 std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i) {
 std::cout << "RValue processed: " << i << std::endl;
}
int main() {
 int a = 0;
 process_value(a);
 process_value(1);
}

运行结果 :

1
2
LValue processed: 0
RValue processed: 1

Process_value 函数被重载,分别接受左值和右值。由输出结果可以看出,临时对象是作为右值处理的。

但是如果临时对象通过一个接受右值的函数传递给另一个函数时,就会变成左值,因为这个临时对象在传递过程中,变成了命名对象。

示例程序 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void process_value(int& i) {
 std::cout << "LValue processed: " << i << std::endl;
}
void process_value(int&& i) {
 std::cout << "RValue processed: " << i << std::endl;
}
void forward_value(int&& i) {
 process_value(i);
}
int main() {
 int a = 0;
 process_value(a);
 process_value(1);
 forward_value(2);
}

运行结果 :

1
2
3
LValue processed: 0
RValue processed: 1
LValue processed: 2

虽然 2 这个立即数在函数 forward_value 接收时是右值,但到了 process_value 接收时,变成了左值。

转移语义的定义

右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。

转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。

通过转移语义,临时对象中的资源能够转移其它的对象里。

在现有的 C++ 机制中,我们可以定义拷贝构造函数和赋值函数。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用。

普通的函数和操作符也可以利用右值引用操作符实现转移语义。

实现转移构造函数和转移赋值函数

以一个简单的 string 类为示例,实现拷贝构造函数和拷贝赋值操作符。

示例程序 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class MyString {
private:
 char* _data;
 size_t   _len;
 void _init_data(const char *s) {
   _data = new char[_len+1];
   memcpy(_data, s, _len);
   _data[_len] = '\0';
 }
public:
 MyString() {
   _data = NULL;
   _len = 0;
 }
 MyString(const char* p) {
   _len = strlen (p);
   _init_data(p);
 }
 MyString(const MyString& str) {
   _len = str._len;
   _init_data(str._data);
   std::cout << "Copy Constructor is called! source: " << str._data << std::endl;
 }
 MyString& operator=(const MyString& str) {
   if (this != &str) {
     _len = str._len;
     _init_data(str._data);
   }
   std::cout << "Copy Assignment is called! source: " << str._data << std::endl;
   return *this;
 }
 virtual ~MyString() {
   if (_data) free(_data);
 }
};
int main() {
 MyString a;
 a = MyString("Hello");
 std::vector<MyString> vec;
 vec.push_back(MyString("World"));
}

运行结果 :

1
2
Copy Assignment is called! source: Hello
Copy Constructor is called! source: World

这个 string 类已经基本满足我们演示的需要。在 main 函数中,实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作。MyString(“Hello”) 和 MyString(“World”) 都是临时对象,也就是右值。虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,有能节省资源申请和释放的时间。这正是定义转移语义的目的。

我们先定义转移构造函数。

1
2
3
4
5
6
7
MyString(MyString&& str) {
   std::cout << "Move Constructor is called! source: " << str._data << std::endl;
   _len = str._len;
   _data = str._data;
   str._len = 0;
   str._data = NULL;
}

和拷贝构造函数类似,有几点需要注意:

1. 参数(右值)的符号必须是右值引用符号,即“&&”。

2. 参数(右值)不可以是常量,因为我们需要修改右值。

3. 参数(右值)的资源链接和标记必须修改。否则,右值的析构函数就会释放资源。转移到新对象的资源也就无效了。

现在我们定义转移赋值操作符。

1
2
3
4
5
6
7
8
9
10
MyString& operator=(MyString&& str) {
   std::cout << "Move Assignment is called! source: " << str._data << std::endl;
   if (this != &str) {
     _len = str._len;
     _data = str._data;
     str._len = 0;
     str._data = NULL;
   }
   return *this;
}

这里需要注意的问题和转移构造函数是一样的。

增加了转移构造函数和转移复制操作符后,我们的程序运行结果为 :

1
2
Move Assignment is called! source: Hello
Move Constructor is called! source: World

由此看出,编译器区分了左值和右值,对右值调用了转移构造函数和转移赋值操作符。节省了资源,提高了程序运行的效率。

有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计转移构造函数和转移赋值函数,以提高应用程序的效率。

标准库函数 std::move

既然编译器只对右值引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用,怎么做呢?标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。

示例程序 :

1
2
3
4
5
6
7
8
9
10
11
12
13
void ProcessValue(int& i) {
 std::cout << "LValue processed: " << i << std::endl;
}
void ProcessValue(int&& i) {
 std::cout << "RValue processed: " << i << std::endl;
}
int main() {
 int a = 0;
 ProcessValue(a);
 ProcessValue(std::move(a));
}

运行结果 :

1
2
LValue processed: 0
RValue processed: 0

std::move在提高 swap 函数的的性能上非常有帮助,一般来说,swap函数的通用定义如下:

1
2
3
4
5
6
   template <class T> swap(T& a, T& b)
   {
       T tmp(a);   // copy a to tmp
       a = b;      // copy b to a
       b = tmp;    // copy tmp to b
}

有了 std::move,swap 函数的定义变为 :

1
2
3
4
5
6
   template <class T> swap(T& a, T& b)
   {
       T tmp(std::move(a)); // move a to tmp
       a = std::move(b);    // move b to a
       b = std::move(tmp);  // move tmp to b
}

通过 std::move,一个简单的 swap 函数就避免了 3 次不必要的拷贝操作。

精确传递 (Perfect Forwarding)

本文采用精确传递表达这个意思。”Perfect Forwarding”也被翻译成完美转发,精准转发等,说的都是一个意思。

精确传递适用于这样的场景:需要将一组参数原封不动的传递给另一个函数。

“原封不动”不仅仅是参数的值不变,在 C++ 中,除了参数值之外,还有一下两组属性:

左值/右值和 const/non-const。 精确传递就是在参数传递过程中,所有这些属性和参数值都不能改变。在泛型函数中,这样的需求非常普遍。

下面举例说明。函数 forward_value 是一个泛型函数,它将一个参数传递给另一个函数 process_value。

forward_value 的定义为:

1
2
3
4
5
6
template <typename T> void forward_value(const T& val) {
 process_value(val);
}
template <typename T> void forward_value(T& val) {
 process_value(val);
}

函数 forward_value 为每一个参数必须重载两种类型,T& 和 const T&,否则,下面四种不同类型参数的调用中就不能同时满足  :

1
2
3
4
5
int a = 0;
 const int &b = 1;
 forward_value(a); // int&
 forward_value(b); // const int&
forward_value(2); // int&

对于一个参数就要重载两次,也就是函数重载的次数和参数的个数是一个正比的关系。这个函数的定义次数对于程序员来说,是非常低效的。我们看看右值引用如何帮助我们解决这个问题  :

1
2
3
template <typename T> void forward_value(T&& val) {
 process_value(val);
}

只需要定义一次,接受一个右值引用的参数,就能够将所有的参数类型原封不动的传递给目标函数。四种不用类型参数的调用都能满足,参数的左右值属性和 const/non-cosnt 属性完全传递给目标函数 process_value。这个解决方案不是简洁优雅吗?

1
2
3
4
5
int a = 0;
const int &b = 1;
forward_value(a); // int&
forward_value(b); // const int&
forward_value(2); // int&&

C++11 中定义的 T&& 的推导规则为:

右值实参为右值引用,左值实参仍然为左值引用。

一句话,就是参数的属性不变。这样也就完美的实现了参数的完整传递。

右值引用,表面上看只是增加了一个引用符号,但它对 C++ 软件设计和类库的设计有非常大的影响。它既能简化代码,又能提高程序运行效率。每一个 C++ 软件设计师和程序员都应该理解并能够应用它。我们在设计类的时候如果有动态申请的资源,也应该设计转移构造函数和转移拷贝函数。在设计类库时,还应该考虑 std::move 的使用场景并积极使用它。

总结

右值引用和转移语义是 C++ 新标准中的一个重要特性。每一个专业的 C++ 开发人员都应该掌握并应用到实际项目中。在有机会重构代码时,也应该思考是否可以应用新也行。在使用之前,需要检查一下编译器的支持情况。

相关主题

  • C++11 标准新特性:Defaulted 和 Deleted 函数:本文介绍了 C++11 标准的两个新特性:defaulted 和 deleted 函数,它们是对 C++ 已有关键字 default 和 delete 的语法扩充,可以帮助开发人员方便地控制编译器的默认动作,如:生成函数、转换等操作。
  • 请参阅 C++11 FAQ,了解各个特性。
  • 请参阅 C++ Standard working draft。
  • 请参阅 C++11 Support in GCC,了解各个特性在 GCC 中的支持情况。
  • AIX and UNIX 专区:developerWorks 的“AIX and UNIX 专区”提供了大量与 AIX 系统管理的所有方面相关的信息,您可以利用它们来扩展自己的 UNIX 技能。
  • AIX and UNIX 专题汇总:AIX and UNIX 专区已经为您推出了很多的技术专题,为您总结了很多热门的知识点。我们在后面还会继续推出很多相关的热门专题给您,为了方便您的访问,我们在这里为您把本专区的所有专题进行汇总,让您更方便的找到您需要的内容。
  • AIX and UNIX 下载中心:在这里你可以下载到可以运行在 AIX 或者是 UNIX 系统上的 IBM 服务器软件以及工具,让您可以提前免费试用他们的强大功能。
  • IBM Systems Magazine for AIX 中文版:本杂志的内容更加关注于趋势和企业级架构应用方面的内容,同时对于新兴的技术、产品、应用方式等也有很深入的探讨。IBM Systems Magazine 的内容都是由十分资深的业内人士撰写的,包括 IBM 的合作伙伴、IBM 的主机工程师以及高级管理人员。所以,从这些内容中,您可以了解到更高层次的应用理念,让您在选择和应用 IBM 系统时有一个更好的认识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/384114.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

打动态库和静态库

一.动态库和静态库的定义 1.静态库     程序在编译链接时把库的代码链接到可执行文件中。程序运行时就不再需要静态库 2.动态库     程序在运行的时候才去链接动态库的代码&#xff0c;多个程序 共享使用代码 3.动态链接     在执行文件之前&#xff0c;外部…

HYSBZ - 2157树链剖分

【题目描述】 HYSBZ - 2157树链剖分 【题目分析】 这道题给出的是边权而不是点权&#xff0c;但是我们分析这个树就会发现每个节点都只有一个父亲&#xff0c;也就是每条边的边权都可以存放在儿子节点上&#xff0c;然后在遍历路径的时候我们在从前往后遍历&#xff0c;但是注…

C++11中的右值引用

http://www.cnblogs.com/yanqi0124/p/4723698.html 在C98中有左值和右值的概念&#xff0c;不过这两个概念对于很多程序员并不关心&#xff0c;因为不知道这两个概念照样可以写出好程序。在C11中对右值的概念进行了增强&#xff0c;我个人理解这部分内容是C11引入的特性中最难以…

BZOJ2115XOR——线性基

【题目描述】 BZOJ2115XOR——线性基 【题目分析】 这道题看完以后很懵逼&#xff0c;人家要是走的很复杂呢&#xff1f;各种绕来绕去怎么办&#xff1f; 首先我们应该注意到一个很明显的道理&#xff1a;重复的路径会和自身抵消&#xff0c;所以我们大可以随便跑&#xff0c;…

单链表的相关操作

1.冒泡排序对单链表进行排序 void LinkListBubbleSort(LinkNode* head) {if(head NULL){ return;//空链表} if(head -> next NULL){ return;//只有一个结点} LinkNode* cur head;//趟数LinkNode* tail NULL;//尾指针LinkNode* tmp head;//次数for(; cur -…

socket网络编程--epoll小结

http://www.cnblogs.com/wunaozai/p/3895860.html 以前使用的用于I/O多路复用为了方便就使用select函数&#xff0c;但select这个函数是有缺陷的。因为它所支持的并发连接数是有限的(一般小于1024)&#xff0c;因为用户处理的数组是使用硬编码的。这个最大值为FD_SETSIZE&#…

进程间通信(匿名管道)

1.进程通信的目的 (1) 数据传输: 一个进程需要将它的数据传输给另一个进程     (2) 资源共享: 多个进程之间共享同样的资源     (3) 通知事件: 一个进程需要向另一个或一组进程发送消息, 通知它们发生了什么事情 2.管道 管道是一种进程之间通信的一种方式, 我们把从…

线性基入门

今天学习了神奇的线性基&#xff0c;主要是在解决异或问题时比较有用。 详细的解释和证明有大佬珠玉在前&#xff0c;如果感兴趣可以移步 补充一下自己的理解&#xff1a; 可以联系线性代数极大无关组进行理解&#xff0c;线性基就相当于异或的向量空间中的极大无关组&#xff…

单例模式及C++实现代码

http://www.cnblogs.com/cxjchen/p/3148582.html 单例模式 单例模式&#xff0c;可以说设计模式中最常应用的一种模式了&#xff0c;据说也是面试官最喜欢的题目。但是如果没有学过设计模式的人&#xff0c;可能不会想到要去应用单例模式&#xff0c;面对单例模式适用的情况&am…

UVALive - 8512——线段树维护线性基

【题目描述】 UVALive - 8512XOR 【题目分析】 这种区间线性基的问题我们可以考虑用线段树维护&#xff0c;线性基的合并的话就直接暴力合并 找到所在区间的线性基后再查找最大的数&#xff0c;我看网上的博客要说消除k的影响什么的&#xff0c;我觉得没有什么必要&#xff0c;…

命名管道

1.命名管道的创建 (1) 通过命令创建 mkfifo filename (2)在程序中创建 int mkfifo(const char* filename, mode_t mode); 2. 命名管道和匿名管道的区别 (1)匿名管道由pipe函数创建并且打开     (2)命名管道有mkfifo函数创建由open函数打开     (3) fifo 之间的两…

HYSBZ - 1101——莫比乌斯反演

【题目描述】 HYSBZ - 1101 【题目分析】 昨天测试出了一道差不多的题目&#xff0c;我只能想到暴力&#xff0c;各种优化&#xff0c;最后都是运行了好久TLE&#xff0c;最后才知道要用到莫比乌斯反演&#xff0c;就想着今天研究一下&#xff0c;得出的结论就是&#xff0c;我…

Linux下I/O多路转接之select --fd_set

http://blog.csdn.net/li_ning_/article/details/52165993 fd_set 你终于还是来了&#xff0c;能看到这个标题进来的&#xff0c;我想&#xff0c;你一定是和我遇到了一样的问题&#xff0c;一样的疑惑&#xff0c;接下来几个小时&#xff0c;我一定竭尽全力&#xff0c;写出我…

BZOJ 2844 | HYSBZ - 2844albus就是要第一个出场——线性基

【题目描述】 BZOJ 2844 | HYSBZ - 2844albus 【题目分析】 题目的意思大概是给一个数列&#xff0c;他有2n个子集&#xff0c;每个子集的元素的异或和构成新的一个数列&#xff0c;排序后问数字Q在这个序列里面的下标。 假如题目是求所有元素的异或和构成一个集合就好弄了&…

CodeForces - 641ELittle Artem and Time Machine——map+树状数组

【题目描述】 CodeForces - 641ELittle Artem and Time Machine 【题目分析】 题目的意思大概是有三种操作 1.在时间t加入一个数字x 2.在时间t删除一个数字x 3.询问在时间t集合里面x的个数 虽然题目描述很简单&#xff0c;但是t和x的范围都是109&#xff0c;我一开始想到的是主…

I/O多路转接之poll 函数

http://blog.csdn.net/li_ning_/article/details/52167224 poll 一、poll()函数&#xff1a; 这个函数是某些Unix系统提供的用于执行与select()函数同等功能的函数&#xff0c;自认为poll和select大同小异&#xff0c;下面是这个函数的声明&#xff1a; [cpp] view plaincopy …

链表相关笔试面试题

1.判断两个链表是否相交 两个链表是否相交可分为以下几种情况     &#xff08;1&#xff09;两个链表都不带环&#xff0c;此时两个链表所对应的最后一个节点是相等的     &#xff08;2&#xff09;两个链表一个带环&#xff0c;一个不带环&#xff0c;两个链表一定…

Linux经典问题—五哲学家就餐问题

http://m.blog.csdn.net/aspenstars/article/details/70149038 一、问题介绍 由Dijkstra提出并解决的哲学家进餐问题(The Dinning Philosophers Problem)是典型的同步问题。该问题是描述有五个哲学家共用一张圆桌&#xff0c;分别坐在周围的五张椅子上&#xff0c;在圆桌上有五…

修改之前的myshell使之支持输入输出重定向

1.open函数     这个函数是打开一个文件&#xff08;文件名叫pathname),以 flag 权限打开&#xff0c;flag 包括了以下几种 O_RDONLY&#xff08;只读&#xff09;, O_WRONLY&#xff08;只写&#xff09;, O_RDWR&#xff08;读写&#xff09;&#xff0c;当文件打开成…

HDU - 6621 K-th Closest Distance——主席树+二分

【题目描述】 HDU - 6621 K-th Closest Distance 【题目分析】 因为看到第kkk大的要求&#xff0c;刚开始的时候一直都在想怎么运用第kkk大来解决问题&#xff0c;但是后来看其他人的博客才发现并不需要用第k大&#xff0c;但是主席树维护权值线段树还是需要的&#xff0c;这…