可拷贝和可移动的概念

在面向对象中,有的类是可以拷贝的,例如车、房等他们的属性是可以复制的,可以调用拷贝构造函数,有点类的对象则是独一无二的,或者类的资源是独一无二的,比如 IO 、 std::unique_ptr等,他们不可以复制,但是可以把资源交出所有权给新的对象,称为可以移动的。

C++11最重要的一个改进之一就是引入了move语义,这样在一些对象的构造时可以获取到已有的资源(如内存)而不需要通过拷贝,申请新的内存,这样移动而非拷贝将会大幅度提升性能。例如有些右值即将消亡析构,这个时候我们用移动构造函数可以接管他们的资源。

移动构造函数和移动赋值函数

考虑这样一个类A,里面的成员i 具有一个500的堆数组

#include <iostream>
#include <cstring> using namespace std; class A{
public:
A():i(new int[500]){
cout<<"class A construct!"<<endl;
}
A(const A &a):i(new int[500]){
memcpy(a.i,i,500*sizeof(int));
cout<<"class A copy!"<<endl;
}
~A(){
delete []i;
cout<<"class A destruct!"<<endl;
} private:
int *i;
}; A get_A_value(){
return A();
}
void pass_A_by_value(A a){ }
int main(){
A a = get_A_value();
return 0;
}

编译时为了看到临时对象拷贝我们关闭了编译器省略复制构造的优化

g++ main.cpp -o main.exe -fno-elide-constructors -std=c++11

运行时可以看到

class A construct!
class A copy!
class A destruct!
class A copy!
class A destruct!
class A destruct!

发生了一次构造和两次拷贝!在每次拷贝中数组都得重新申请内存,而被拷贝后的对象很快就会析构,这无疑是一种浪费。

我们在类中加上移动构造函数:

...
#include <iostream>
A(A &&a)noexcept
:i(a.i)
{
a.i = nullptr;
cout<< "class A move"<<endl;
}
...

然后编译、执行;可以看到输出为

class A construct!
class A move
class A destruct!
class A move
class A destruct!
class A destruct!

原先的两次构造变成了两次移动!!在移动构造函数中,我们做了什么呢,我们只是获取了被移动对象的资源(这里是内存)的所有权,同时把被移动对象的成员指针置为空(以避免移动过来的内存被析构),这个过程中没有新内存的申请和分配,在大量对象的系统中,移动构造相对与拷贝构造可以显著提高性能!这里noexcept告诉编译器这里不会抛出异常,从而让编译器省一些操作(这个也是保证了STL容器在重新分配内存的时候(知道是noexpect)而使用移动构造而不是拷贝构造函数),通常移动构造都不会抛出异常的。

@note: 这里仅仅为了演示,用 -fno-elide-constructions 关闭了g++编译器会省略函数返回值时临时对象的拷贝的优化。虽然编译器很多时候可以为我们进行优化,有些时候编译器优化不了的还是需要了解和运用移动语义的。

除了移动构造函数,移动赋值运算符应该一并给写出来。

   A &operator =(A &&rhs) noexcept{
// check self assignment
if(this != &rhs){
delete []i;
i = rhs.i;
rhs.i = nullptr;
}
cout<< "class A move and assignment"<<std::endl;
return *this;
}

小结移动构造和移动赋值

小结一下移动构造函数和移动赋值函数的书写要诀:

  1. 偷梁换柱直接“浅拷贝”右值引用的对象的成员;
  2. 需要把原先右值引用的指针成员置为 nullptr,以避免右值在析构的时候把我们浅拷贝的资源给释放了;
  3. 移动构造函数需要先检查一下是否是自赋值,然后才能先delet自己的成员内存再浅拷贝右值的成员,始终记住第2条。

关于构造函数这部分有很多best practice :搜索“三五法则”、 “copy and swap”、 "move and swap" 了解详情

std::move()

std::move(lvalue) 的作用就是把一个左值转换为右值。关于左右值的含义我们上一篇博客C++11的右值引用进行过阐述。

int lv = 4;
int &lr = lv;// 正确,lr是l的左值引用
int &&rr = lv; // 错误,不可以把右值引用绑定到一个左值

如果使用std::move 函数

   int &&rr = std::move(lv);  // 正确,把左值转换为右值

可以看到 std::move的作用是把左值转换为右值的。

让我们看一看 std::move 的源码实现:

// FUNCTION TEMPLATE move
template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

可以看到std::move 是一个模板函数,通过remove_\reference_t获得模板参数的原本类型,然后把值转换为该类型的右值。用C++大师 Scott Meyers 的在《Effective Modern C++》中的话说, std::move 是个cast ,not a move.

值得注意的是: 使用move意味着,把一个左值转换为右值,原先的值不应该继续再使用(承诺即将废弃)

使用 std::move 实现一个高效的 swap 函数

我们可以使用 move语义实现一个 交换操作,swap;

在不使用 Move 语义的情况下

swap(A &a1, A &a2){
A tmp(a1); // 拷贝构造函数一次,涉及大量数据的拷贝
a1 = a2; // 拷贝赋值函数调用,涉及大量数据的拷贝
a2 = tmp; // 拷贝赋值函数调用,涉及大量数据的拷贝
}

如果使用 Move语义,即加上移动构造函数和移动赋值函数:

void swap_A(A &a1, A &a2){
A tmp(std::move(a1)); // a1 转为右值,移动构造函数调用,低成本
a1 = std::move(a2); // a2 转为右值,移动赋值函数调用,低成本
a2 = std::move(tmp); // tmp 转为右值移动给a2
}

可以看到move语义确实可以提高性能,事实上, move语义广泛地用于标准库的容器中。C++11标准库里的std::swap 也是基于移动语义实现的。

说到了 swap, 那就不得不说一下啊 move-and-swap 技术了

Move and swap 技巧

看下面一段代码,实现了一个 unique_ptr ,和标准的std::unqiue_ptr的含义一致,智能指针的一种。

template<typename T>
class unique_ptr
{
T* ptr; public: explicit unique_ptr(T* p = nullptr)
{
ptr = p;
} ~unique_ptr()
{
delete ptr;
} // move constructor
unique_ptr(unique_ptr&& source) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
} /* unique_ptr& operator=(unique_ptr&& source) // 这里使用右值引用
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
} */ // move and swap idiom replace the move assignment operator
unique_ptr& operator=(unique_ptr rhs) // 这里不用引用,会调用移动构造函数
{
std::swap(ptr, rhs.ptr);
// std::swap(*this,rhs) // is also ok
return *this;
} T* operator->() const
{
return ptr;
} T& operator*() const
{
return *ptr;
}
};

在这里如果要按照常规办法写移动赋值函数,函数体内需要写一堆检查自赋值等冗长的代码。使用 move-and-swap语义,只用简短的两行就可以写出来。 在移动赋值函数中 source 是个局部对象,这样在形参传递过来的时候必须要调用拷贝构造函数(这里没有实现则不可调用)或者移动构造函数

,(事实上仅限右值可以传进来了)。然后 std::swap 负责把原先的资源和source 进行交换,完成了移动赋值。这样写节省了很多代码,很优雅。

move-and-swap 和 copy-and-swap 见我另外一篇博客。

参考

以下两篇是我查阅资料的时候在 stackoverflow 上发现觉得写得非常到位的:

C++ 11的移动语义的更多相关文章

  1. c++11的右值引用、移动语义

    对于c++11来说移动语义是一个重要的概念,一直以来我对这个概念都似懂非懂.最近翻翻资料感觉突然开窍,因此记下.其实搞懂之后就会发现这个概念很简单,并无什么高深的地方. 先说说右值引用.右值一般指的是 ...

  2. 对C++11中的`移动语义`与`右值引用`的介绍与讨论

    本文主要介绍了C++11中的移动语义与右值引用, 并且对其中的一些坑做了深入的讨论. 在正式介绍这部分内容之前, 我们先介绍一下rule of three/five原则, 与copy-and-swap ...

  3. move语义和右值引用

    C++11支持move语义,用以避免非必要拷贝和临时对象. 具体内容见收藏中的“C++右值引用” .

  4. c++11之右值引用

    本文大部分来自这里,并不是完全着行翻译,如有不明白的地方请参考原文. 在c++中,创建临时对象的开销对程序的影响一直很大,比如以下这个例子: String getName(){ return “Kia ...

  5. 使用C++11封装线程池ThreadPool

    读本文之前,请务必阅读: 使用C++11的function/bind组件封装Thread以及回调函数的使用 Linux组件封装(五)一个生产者消费者问题示例   线程池本质上是一个生产者消费者模型,所 ...

  6. 透彻理解C++11新特性:右值引用、std::move、std::forward

    目录 浅拷贝.深拷贝 左值.右值 右值引用类型 强转右值 std::move 重新审视右值引用 右值引用类型和右值的关系 函数参数传递 函数返还值传递 万能引用 引用折叠 完美转发 std::forw ...

  7. iOS经典面试题总结--内存管理

    iOS经典面试题总结--内存管理 内存管理 1.什么是ARC? ARC是automatic reference counting自动引用计数,在程序编译时自动加入retain/release.在对象被 ...

  8. HTML5 基础

    1.HTML5 简介 HTML5 是最新的 HTML 标准,他是万维网的核心语言.标准通用标记语言下的一个应用“超文本标记语言”. HTML 的上一个标准 HTML4.01 诞生于 1999年,他的第 ...

  9. ER 和 数据库关系模式

    http://lianghuanyue123.blog.163.com/blog/static/130423244201162011850600/ 我们眼下所接触的数据库基本上是关系数据库,关系数据库 ...

随机推荐

  1. CodeForces 830B - Cards Sorting

    将每个数字的位置存进该数字的vector中 原数组排个序从小到大处理,每次在vector里二分找到距离当前位置“最远”的位置(相差最大),更新答案 树状数组维护每个数字现在的位置和原位置之差 #inc ...

  2. hdu 6078 Wavel Sequence

    题 OvO http://acm.hdu.edu.cn/showproblem.php?pid=6078 (2017 Multi-University Training Contest - Team ...

  3. 【Winfrom-TreeView】 跟随系统改变Style

    C#: public class NativeTreeView : System.Windows.Forms.TreeView { [DllImport("uxtheme.dll" ...

  4. java上传视频文件

    需求:项目要支持大文件上传功能,经过讨论,初步将文件上传大小控制在500M内,因此自己需要在项目中进行文件上传部分的调整和配置,自己将大小都以501M来进行限制. 第一步: 前端修改 由于项目使用的是 ...

  5. POJ 2109 Power of Cryptography 数学题 double和float精度和范围

    Power of Cryptography Time Limit: 1000MS Memory Limit: 30000K Total Submissions: 21354 Accepted: 107 ...

  6. HDU 5793 A Boring Question ——(找规律,快速幂 + 求逆元)

    参考博客:http://www.cnblogs.com/Sunshine-tcf/p/5737627.html. 说实话,官方博客的推导公式看不懂...只能按照别人一样打表找规律了...但是打表以后其 ...

  7. log4j 多进程配置要注意的

    多进程写日志文件 方法一: 解决log4j公用配置文件,多进程同时写同一个log文件,因存在操作系统pv操作问题, 导致部分日志丢失.解决方案是不同的进程写不同的log文件 测试于:Log4j 1.2 ...

  8. CTF MD5之守株待兔,你需要找到和系统锁匹配的钥匙

    这是提示 从系统锁下手,通过get方式key字段提交答案,直到您的钥匙与系统锁相等则成功. 点开链接可以发现有两串字符,而且系统的秘钥是一直在变化的 题目中已经给了MD5加密,那么用MD5解密发现您的 ...

  9. (十七)C语言之变量

  10. Nginx事件管理之概念描述

    1. Nginx事件管理概述 首先,Nginx定义了一个核心模块ngx_events_module,这样在Nginx启动时会调用ngx_init_cycle方法解析配置项,一旦在 nginx.conf ...