C++11 右值引用和转移语义

新特性的目的

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

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

左值与右值的定义

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

请看下列示例 :

1.简单的赋值语句

int i = 0;

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

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

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

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

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

const int &a = 1;

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

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

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

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

左值和右值的语法符号

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

示例程序 :

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);
}

运行结果 :

LValue processed: 0
 RValue processed: 1

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

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

示例程序 :

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);
}

运行结果 :

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

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

转移语义的定义

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

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

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

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

如果转移构造函数和转移拷贝操作符没有定义,那么就遵循现有的机制,拷贝构造函数和赋值操作符会被调用。

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

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

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

示例程序 :

C++11 右值引用和转移语义C++11 右值引用和转移语义
#include
#include
#include
#include
#include
using namespace std;

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 vec;
    vec.push_back(MyString("World"));
}

View Code

运行结果 :

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

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

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

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

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

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;
}

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

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

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

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

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

标准库函数 std::move

既然编译器只对右值引用才能调用转移构造函数和转移赋值函数,而所有命名对象都只能是左值引用,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用,怎么做呢?

标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。

从实现上讲:等同与一个类型转换:static_cast

值得一提的是,被转化的左值其生命周期并没有随着左右值的转化而改变,如果你认为std::move转化的左值变量会立刻被析构,那么你肯定会很失望(使用经move后的变量会发生严重的运行时错误)。

C++11 右值引用和转移语义C++11 右值引用和转移语义
#include

using namespace std;

class Moveable
{
public:
    Moveable(std::string name):name(name){}
    ~Moveable() { std::cout << name << " destory" << std::endl; delete i; }
    Moveable(const Moveable& m)
    :i(new int(*m.i))
    ,name("copy_obj"){

    }
    Moveable(Moveable&& m)
    :i(m.i)
    ,name("move_obj") {
        m.i = nullptr;
    }

    int *i;
    std::string name;
};

int main()
{
    Moveable a("obj_a");
    Moveable c(std::move(a));
    //std::cout << *a.i << std::endl;

    return 0;
}

View Code

输出:

move_obj destory
obj_a destory

示例程序 :

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));
}

运行结果 :

LValue processed: 0
 RValue processed: 0

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

template  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 函数的定义变为 :

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 的定义为:

template void forward_value(const T& val) {
    process_value(val);
}
template  void forward_value(T& val) {
    process_value(val);
}

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

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

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

template void forward_value(T&& val) {
    process_value(val);
}

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

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 的使用场景并积极使用它。

Original: https://www.cnblogs.com/DswCnblog/p/6550568.html
Author: 滴水瓦
Title: C++11 右值引用和转移语义

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/535404/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 用C/C++实现对STORM的执行信息查看和控制

    近期公司有个需求。须要在后端应用server上实时获取STORM集群的执行信息和topology相关的提交和控制,经过几天对STORM UI和CMD源代码的分析,得出能够通过其th…

    C++ 2023年5月29日
    078
  • C++11 动态内存管理

    内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C+…

    C++ 2023年5月29日
    050
  • 2022年第十三届蓝桥杯C++B组国赛思路以及部分代码

    404. 抱歉,您访问的资源不存在。 可能是网址有误,或者对应的内容被删除,或者处于私有状态。 代码改变世界,联系邮箱 contact@cnblogs.com 园子的商业化努力-困…

    C++ 2023年5月29日
    046
  • C和C++混合编程中的extern “C” {}

    在用C++的项目源码中,经常会不可避免的会看到下面的代码: 它到底有什么用呢,你知道吗?而且这样的问题经常会出现在面试or笔试中。下面我就从以下几个方面来介绍它: 1、#ifdef…

    C++ 2023年5月29日
    055
  • 聊聊 C++ 中的几种智能指针 (上)

    一:背景 我们知道 C++ 是手工管理内存的分配和释放,对应的操作符就是 new/delete 和 new[] / delete[], 这给了程序员极大的自由度也给了我们极高的门槛…

    C++ 2023年5月29日
    081
  • c++ typedef和#define的作用范围

    typedef: 如果放在所有函数之外,它的作用域就是从它定义开始直到文件尾; 如果放在某个函数内,定义域就是从定义开始直到该函数结尾; #define: 不管是在某个函数内,还是…

    C++ 2023年5月29日
    096
  • c++11 auto 与 decltype 详解

    一. auto简介 编程时候常常需要把表达式的值付给变量,需要在声明变量的时候清楚的知道变量是什么类型。然而做到这一点并非那么容易(特别是模板中),有时候根本做不到。为了解决这个问…

    C++ 2023年5月29日
    061
  • C++ #ifndef/#define/#endif解释

    作用:防止头文件的重复包含和编译 ifndef x define x endif 比如说有一个头文件叫head.h,这是一个通用的头文件,然后我又定义了两个自己用的头文件,分别叫l…

    C++ 2023年5月29日
    041
  • 腾讯研发类笔试面试试题(C++方向)(转)

    原文转自 https://www.cnblogs.com/freebird92/p/9595244.html 1、C和C++的特点与区别? 答: (1)C语言特点:1.作为一种面向…

    C++ 2023年5月29日
    044
  • (筆記) 如何讀取binary file某個byte連續n byte的值? (C/C++) (C)

    Abstract通常公司為了保護其智慧財產權,會自己定義檔案格式,其header區會定義每個byte各代表某項資訊,所以常常需要直接對binary檔的某byte直接進行讀取,且連續…

    C++ 2023年5月29日
    049
  • Emacs中使用company + irony实现C++代码补全

    下面是主要配置,一些插件可能需要emacs版本 >= 25.1 对于Irony的话,需要在emacs中手动执行 M-x irony-install-server 来安装好ir…

    C++ 2023年5月29日
    041
  • c++ 11新特性学习1

    static_assert 静态断言,特点是编译期的断言检查 assert 运行时期的断言检查 二者参数用法相同 Original: https://www.cnblogs.com…

    C++ 2023年5月29日
    055
  • C++内存分配方式详解——堆、栈、自由存储区、全局/静态存储区和常量存储区

    栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现…

    C++ 2023年5月29日
    068
  • Prim算法(二)之 C++详解

    普里姆(Prim)算法,是用来求加权连通图的最小生成树的算法。 基本思想对于图G而言,V是所有顶点的集合;现在,设置两个新的集合U和T,其中U用于存放G的最小生成树中的顶点,T存放…

    C++ 2023年5月29日
    053
  • c++为什么要面向对象?

    前言 c和c++的区别是什么?不可置否,最重要的就是c++的编程思想是面向对象,而c的编程思想是面向过程,这是它们的本质区别,如果你在使用c++编程时使用的还是面向过程的编程思想,…

    C++ 2023年5月29日
    060
  • c++ set与unordered set的区别

    c++ std中set与unordered_set区别和map与unordered_map区别类似,其底层的数据结构说明如下: 1、set基于红黑树实现,红黑树具有自动排序的功能,…

    C++ 2023年5月29日
    050
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球