c++11move语意的便捷理解

本文是一篇回答,原文链接为:http://stackoverflow.com/questions/3106110/what-are-move-semantics(第一个回答)。对于理解c++11的move概念较有帮助,我用(译注:)补充了自己的一些看法,而(原文:)表示原作者的括号。
正文如下:

我想到一种很容易的例子来理解move语意。我们从一个非常简单的字符串类开始,它只包含一个指向一堆内存块的指针:

  #include<cstring>
  #include<algorithm>
  class string
  {
      char*  data;
    public:
        string(const char*  p)
        {
            size_t size=strlen(p)+1;
            data=new char[size];
            memcpy(data,p,size);
        }
  }

既然我们要自己管理内存,那么我们就需要遵循规则三译注:规则三即:如果你需要显式地声明一下三者中的一个:析构函数、拷贝构造函数或者是拷贝赋值操作符,那么你需要显式的声明所有这三者)。我先把析构函数和拷贝构造函数写了,赋值操作符稍候再说:

~string()
    {
        delete[] data;
    }

    string(const string& that)
    {
        size_t size = strlen(that.data) + 1;
        data = new char[size];
        memcpy(data, that.data, size);
    }

拷贝构造函数实现了复制string对象的方法,即通过参数’const string& that‘bind所有string类型的表达式来copy,如下例:

string a(x);                                    // Line 1
string b(x + y);                                // Line 2
string c(some_function_returning_a_string());   // Line 3

现在进入理解move语意的关键。注意只有在第一行中我们拷贝“x”是真的需要深拷贝的(译注:如不理解深拷贝请先理解下),因为我们的“x”在之后可能还会需要用到,如果不改变的话你会很惊讶的发现“x”在不知何时就被改变了(译注:还是解释下吧,x之后可能会用到,如果不深拷贝,即a改变了,x也会改变,这样会造成一系列你不好找的问题)。你注意到我说了三次“x”没有(原文:其实是四次,如果包括这句话的话),并且每次都是指同一对象?我们把这个“x”称为“lvalues”(译注:左值)。

在第二行和第三行的就不是“lvalues”,而是“rvalues”(译注:右值),因为作为参数的string对象只是临时的,编译器无法再之后使用它(译注:如第二行,会有一个x+y产生的临时变量,再传到拷贝构造函数当参数)。“rvalues”表示临时变量将在下一个分号的时候销毁(原文:更精确的来说,在表达式末尾不再使用“rvalues”的时候)。这相当重要,因为这意味着在初始化“b”和“c”的时候,我们可以对这个临时变量做任何事情,因为编译器不知道我们做了什么!

c++0x(译注:是c++11发布前的草稿名字)引入了一个新的机制叫“rvalues reference”(译注:右值引用),其中一点就是使我们可以将右值作为参数进行函数重载,我们要做的仅仅是写一个“rvalues reference”参数的构造函数。在这个构造函数下我们可以做任何想做的事情,只要我们给他个有效的状态值:

    string(string&& that)   // string&& is an rvalue reference to a string
    {
        data = that.data;
        that.data = nullptr;
    }

在这里我们做了什么呢?不需要将堆数据进行深拷贝,我们只需要拷贝指针(译注:拷贝指针所指向的内存地址),并且将原始的指针设为空。事实上,这里我们把原string对象的数据“偷了”过来。再次声明,关键在于理解在任何情况下编译器检测到源的修改(译注:句式是这样,这句话略费解,但不影响整体意思),而在这里我们其实并没有真的进行拷贝,更适合称之为“move constructor”(译注:移动构造函数)。他的任务是把一个对象move给另一个对象,而不是拷贝。

祝贺,你现在已经理解了move语意的基本知识!接下来继续了解开头提到的赋值运算符。如果你对copy-and-swap概念了解的不深,那么先去看完来(译注:原理为:利用拷贝构造函数生成一个临时拷贝,然后使用swap函数将此拷贝对象与旧数据交换。然后临时对象被析构,旧数据消失。我们就拥有了新数据的拷贝。附一篇已经翻译好了的译文:连接)。因为它是c++一个非常安全的用语。

 string& operator=(string that)
    {
        std::swap(data, that.data);
        return *this;
    }
};

哈?那是个啥?“怎么没有右值引用?”你可能会觉得疑惑。“因为我们在这儿根本就不需要!”——这就是我的答案 :)

注意我们的参数传递是值传递“that”,也就是说“that”像任意其它string对象那样已经被初始化了。那么问题来了,“that”应该怎样被初始化呢?在以往的c++98时,答案是“通过拷贝构造函数”。而在c++11中,编译器将根据操作符参数是“lvalue”还是“rvalue”来选择是调用拷贝构造还是move构造函数。

所以假如你说“a=b”,“that”将被拷贝构造函数初始化(原文:因为这个时候b是一个“lvalue”),然后和赋值运算符交换一个刚创建的,深拷贝临时对象。这就是copy-and-swap的真实语意——新建一个临时对象,拷贝,然后交换内容,最后返回内容销毁临时对象,并没有什么新奇的地方。

但假如你说“a=x+y”,这个时候that将被move构造函数初始化(原文:因为这个时候“x+y”是一个“rvalue”),所以这时候没有涉及到深拷贝,只有move操作,“that”依然是一个独立的对象,但是它仅仅是创建,因为他的数据不需要拷贝,只是移动了一下。也没有必要拷贝他,因为“x+y”是一个“rvalue”。强调下,string对象使用“rvalue”时只是move。

总结下,拷贝构造函数是深拷贝,因为他的源必须保持不变。而move构造函数,仅仅是拷贝指针,并且将原始的指针设为空。这样“置空”源对象是没问题的,因为编译器没办法再次使用这种对象。

我希望这个例子能让人明白要点。其实还有许多关于“rvalue reference”和move的概念。但我为了简单省略了,有兴趣的可以查看这个:http://stackoverflow.com/questions/3106110/what-are-move-semantics/11540204#11540204

(译注:第一次译文,难免有失偏颇,有疑问可以指出,谢谢)

  • 9
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值