二十分钟弄懂C++11 的 rvalue reference (C++ 性能剖析 (5))

C++ 11加了许多新的功能。其中对C++性能和我们设计classconstructorassignment可能产生重大影响的非rvalue reference莫属!我看了不少资料,能说清它的不多。下面我企图用简单的例子来说明,希望读者能够理解并应用这一重要的语言构造。

1.rvalue reference 是reference (即指针)

比如下面两条语句的语义完全一样:

int &&p = 3;             // line 1

const int &cp = 3      // line 2

2. rvalue reference 指向临时变量

上面的line1line2的共同点是,他们都指向临时变量。所不同的是下面两句:

p = 5;       // p 的内容变成了5

cp = 5;      // 编译出错:cp 不能改动(常数) 

3.rvalue reference可以简化moving 语义 – 提高object 拷贝性能

很好,我们现在可以通过rvalue reference修改(阴暗中的)临时变量了。那么这有什么用呢?目前C++11所宣称的最主要的应用就是所谓的“moving semantics (迁移语义)”。请看下面例子:

class SimpleString

{

       char * _ptr;

public:

       SimpleString(const char *p);

       SimpleString(const SimpleString & another);

       ~SimpleString();

       operator const char * () { return _ptr; }

       SimpleString & operator = (const SimpleString & another);

       static void Test();

private:

       void GetStr(const char *p);

};

SimpleString::SimpleString(const char *p): _ptr(nullptr)

{

       GetStr(p);

}

SimpleString::SimpleString(const SimpleString & another): _ptr(nullptr)

{

       GetStr(another._ptr);

}

void SimpleString::GetStr(const char *p)

{

       if (_ptr)

             delete [] _ptr;

 

       size_t l= ::strlen(p);

       _ptr = new char[l+1];

       ::strcpy_s(this->_ptr, l+1, p);

} 

SimpleString::~SimpleString()

{

       if (_ptr)

       {

             delete [] _ptr;

             printf("SimpleString d'tr called for ");

       }

}

SimpleString & SimpleString::operator = ( const SimpleString & another)

{

       GetStr(another._ptr);

       return *this;

}

namespace

{

      // simple string factory

       SimpleString CreateString()

       {

             SimpleString temp("A temp string created!");

             return temp;

       }

} 

void SimpleString::Test()

{

       SimpleString ret = CreateString();

       printf("ret is: &s ", ret);

}

上面是一个为了试验用的简单string class。 假设我们有一个函数CreateString, 返回一个创建的SimpleString 值。然后赋给接受变量ret。 这个简单的逻辑有什么问题呢?

这里就是临时变量copy constructor的问题。我们这里用了SimpleString::SimpleString(const SimpleString & another),它用GetStr来构建一个新的指针_ptr。然后将临时变量的_ptr所指内容拷贝过来。

这是常见的做法,但是很昂贵的。CreateString函数已经构建了一个有效的_ptr,为什不能拷贝指针呢?

原来,因为CreateString里的temp变量是临时变量,它在CreateString出口时将会被销毁,除非我们能获取他的referencepointer,然后将它的_ptr设为null。这是个好主意,我们再加一个函数: 

void SimpleString::MoveStr(SimpleString & another)

{

       if (this->_ptr)

             delete this->_ptr;

 

       this->_ptr = another._ptr;

       another._ptr = nullptr;

}

然后把copy constructor 改写:

SimpleString::SimpleString(const SimpleString & another): _ptr(nullptr)

{

       MoveStr(const_cast<SimpleString &>(another)); // line 100

}

这样一来,我们就只构建一次_ptr了,测试的结果也证明了这一点。

上面讲的和rvalue reference有何关系呢?

我对line 100的方案不太满意:

1) 我们改变了原来copy constructor的常规意义,现在只要你赋值与另一变量,你就失去了你自己的值。我们希望这个功能只适合于“临时变量”。

2) const_cast 不太好,不美观。

现在,我们因该悟出rvalue reference的意义了吧?

根据第二节,rvalue reference是指向临时变量的,正好是用于指向CreateString产生的临时变量。

原来,只要我们在SimpleString里加一个moving copy constructor(注意&&): 

SimpleString::SimpleString(SimpleString && another): _ptr(nullptr)

{

       MoveStr(another);

}

我们便无需更改SimpleString::SimpleString(const SimpleString & another)了。C++编译自动地在这一行SimpleString ret = CreateString() call 我们的moving constructor SimpleString::SimpleString(SimpleString && another), 而不是我们的copy constructor.

大家不妨试试!

总结

C++11利用rvalue reference,使我们可以方便地实现 moving constructor 语义。这对上述类似的问题(特别是std里的container用法)提供了解决C++传统的临时变量拷贝的功能隐患。

附录:修改后的代码

// header: RValueRef.h

class SimpleString

{

         char * _ptr;

public:

         SimpleString(const char *p);

         SimpleString(const SimpleString & another);

         SimpleString(SimpleString && another); // moving constructor

 

         ~SimpleString();

         operator const char * () { return _ptr; }

         SimpleString & operator = (const SimpleString & another);

         SimpleString & SimpleString::operator = ( SimpleString && another);

 

         static void Test();

 

private:

         void GetStr(const char *p);

         void MoveStr(SimpleString & another);

};

// C++: rvalue.cpp

#include "stdafx.h"

#include <string.h>

#include <stdlib.h>

#include <stdio.h>

#include <errno.h>

#include "RValueRef.h"

 

SimpleString::SimpleString(const char *p): _ptr(nullptr)

{

         GetStr(p);

}

 

SimpleString::SimpleString(const SimpleString & another): _ptr(nullptr)

{

         GetStr(another._ptr);

}

 

// Moving constructor helps move temp var’s _ptr to ourselves.

SimpleString::SimpleString(SimpleString && another): _ptr(nullptr)

{

         MoveStr(another);

}

void SimpleString::GetStr(const char *p)

{

         if (_ptr)

                 delete [] _ptr;

 

         size_t l= ::strlen(p);

         _ptr = new char[l+1];

         ::strcpy_s(this->_ptr, l+1, p);

}

SimpleString::~SimpleString()

{

         if (_ptr)

         {

                 printf("SimpleString d'tr called for '%s' ", _ptr);

                 delete [] _ptr;

         }

}

SimpleString & SimpleString::operator = ( const SimpleString & another)

{

         GetStr(another._ptr);

         return *this;

}

SimpleString & SimpleString::operator = ( SimpleString && another)

{

         MoveStr(another);

         return *this;

}

void SimpleString::MoveStr(SimpleString & another)

{

         if (this->_ptr)

                 delete this->_ptr;

 

         this->_ptr = another._ptr;

         another._ptr = nullptr; // don’t forget to do this

}

namespace

{

          SimpleString CreateString()

         {

                 SimpleString temp("A temp string created!");

                 return temp;

         }

 

}

void SimpleString::Test()

{

         SimpleString ret = CreateString();

         printf("ret is: &s ", ret);

}