读书频道 > 网站 > 网页设计 > 深入应用C++11:代码优化与工程级应用
2.1.2 右值引用优化性能,避免深拷贝
15-07-07    下载编辑
收藏    我要投稿   
在StackOverflow的最近一次世界性调查中,C++11在所有的编程语言中排名第二, C++11受到程序员的追捧是毫不意外的,因为它就像C++之父Bjarne Stroustrup说的:它看起来就像一门新的语言。C++11新增加了相当多的立即去当当网订购

对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除,比如下面的代码:

class A
{
public:
         A() :m_ptr(new int(0))
         {
         }

         ~A()
         {
                   delete m_ptr;
         }

private:
         int* m_ptr;
};


// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
         A a;
         A b;
         if (flag)
                   return a;
         else
                   return b;
}

int main()
{
         A a = Get(false);   // 运行报错
}

在上面的代码中,默认构造函数是浅拷贝,a和b会指向同一个指针m_ptr,在析构的时候会导致重复删除该指针。正确的做法是提供深拷贝的拷贝构造函数,比如下面的代码(关闭返回值优化的情况下):

class A

{
public:
         A() :m_ptr(new int(0))
         {
                   cout << "construct" << endl;
         }

         A(const A& a):m_ptr(new int(*a.m_ptr)) // 深拷贝
         {
                   cout << "copy construct" << endl;
         }
         ~A()
         {
                   cout << "destruct" << endl;
                   delete m_ptr;
         }

private:
         int* m_ptr;
};

int main()
{
         A a = Get(false);   // 运行正确
}
上面的代码将输出:
construct
construct
copy construct
destruct
destruct
destruct

这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的Get函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:

class A
{
public:
         A() :m_ptr(new int(0))
         {
                   cout << "construct" << endl;
         }

         A(const A& a):m_ptr(new int(*a.m_ptr)) // 深拷贝
         {
                   cout << "copy construct" << endl;
         }

         A(A&& a) :m_ptr(a.m_ptr)
         {
                   a.m_ptr = nullptr;
                   cout << "move construct: " << endl;
         }

         ~A()
         {
                   cout << "destruct" << endl;
                   delete m_ptr;
         }

private:
         int* m_ptr;
};

int main()
{
         A a = Get(false); // 运行正确
}
上面的代码将输出:
construct
construct
move construct
destruct
destruct
destruct

上面的代码中没有了拷贝构造,取而代之的是移动构造(Move Construct)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的A&&用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义(move语义),右值引用的一个重要目的是用来支持移动语义的。

移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高C++应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响。

以代码清单2-2所示为示例,实现拷贝构造函数和拷贝赋值操作符。

代码清单2-2 MyString类的实现

class MyString {
private:
         char* m_data;
         size_t   m_len;
         void copy_data(constchar *s) {
                   m_data = newchar[m_len+1];
                   memcpy(_data, s, m_len);
                   m_data[_len] = '\0';
         }
public:
         MyString(){
                   m_data = NULL;
                   m_len = 0;
         }

         MyString(const char* p) {
                   m_len = strlen (p);
                   copy_data(p);
         }

         MyString(const MyString& str) {
                   m_len = str.m_len;
                   copy_data(str.m_data);
                   std::cout <<"Copy Constructor is called! source: "<< str.m_data
                       << std::endl;
         }

         MyString&operator=(const MyString& str) {
                  if (this != &str) {
                               m_len = str.m_len;
                               copy_data(str._data);
                  }
                  std::cout <<"Copy Assignment is called! source: "<< str.m_data
                      << std::endl;
                  return *this;
         }

         virtual ~MyString() {
                   if (m_data) free(m_data);
         }
};
void test() {
         MyString a;
         a = MyString("Hello");
         std::vector<MyString> vec;
         vec.push_back(MyString("World"));
}

实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作。MyString("Hello")和MyString("World")都是临时对象,也就是右值。虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值函数,造成了没有意义的资源申请和释放的操作。如果能够直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间。这正是定义移动语义的目的。

用C++11的右值引用来定义这两个函数,如代码清单2-3所示。

代码清单2-3 MyString的移动构造函数和移动赋值函数

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

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;
}
再看一个简单的例子,代码如下:
struct Element
{
            Element(){}
            // 右值版本的拷贝构造函数
            Element(Element&& other) : m_children(std::move(other.m_children)){}
            Element(const Element& other) : m_children(other.m_children){}
private:
            vector<ptree> m_children;
};
这个Element类提供了一个右值版本的构造函数。这个右值版本的构造函数的一个典型应用场景如下:
void Test()
{
         Element t1 = Init();
         vector<Element> v;
         v.push_back(t1);
         v.push_back(std::move(t1));
}

先构造了一个临时对象t1,这个对象中一个存放了很多Element对象,数量可能很多,如果直接将这个t1用push_back插入到vector中,没有右值版本的构造函数时,会引起大量的拷贝,这种拷贝会造成额外的严重的性能损耗。通过定义右值版本的构造函数以及std::move(t1)就可以避免这种额外的拷贝,从而大幅提高性能。

有了右值引用和移动语义,在设计和实现类时,对于需要动态申请大量资源的类,应该设计右值引用的拷贝构造函数和赋值函数,以提高应用程序的效率。需要注意的是,我们一般在提供右值引用的构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造。

需要注意的一个细节是,我们提供移动构造函数的同时也会提供一个拷贝构造函数,以防止移动不成功的时候还能拷贝构造,使我们的代码更安全。

点击复制链接 与好友分享!回本站首页
分享到: 更多
您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:1.3 功能
下一篇:1.5 小结
相关文章
图文推荐
JavaScript网页动画设
1.9 响应式
1.8 登陆页式
1.7 主题式
排行
热门
文章
下载
读书

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做最好的IT技术学习网站