C++11新特性之 Move semantics(移动语义)

#include <iostream>

void f(int& i) { std::cout << "lvalue ref: " << i << "\n"; }
void f(int&& i) { std::cout << "rvalue ref: " << i << "\n"; }

int main()
{
    int i = 77;
    f(i);    // lvalue ref called
    f(99);   // rvalue ref called

    f(std::move(i));  // 稍后介绍

    return 0;
}

实际上,右值引用注意用于创建移动构造函数和移动赋值运算。

移动构造函数类似于拷贝构造函数,把类的实例对象作为参数,并创建一个新的实例对象。

但是 移动构造函数可以避免内存的重新分配,因为我们知道右值引用提供了一个暂时的对象,而不是进行copy,所以我们可以进行移动。

换言之,在设计到关于临时对象时,右值引用和移动语义允许我们避免不必要的拷贝。我们不想拷贝将要消失的临时对象,所以这个临时对象的资源可以被我们用作于其他的对象。

右值就是典型的临时变量,并且他们可以被修改。如果我们知道一个函数的参数是一个右值,我们可以把它当做一个临时存储。这就意味着我们要移动而不是拷贝右值参数的内容。这就会节省很多的空间。

#include <iostream>
#include <algorithm>

class A
{
public:

    // Simple constructor that initializes the resource.
    explicit A(size_t length)
        : mLength(length), mData(new int[length])
    {
        std::cout << "A(size_t). length = "
        << mLength << "." << std::endl;
    }

    // Destructor.
    ~A()
    {
    std::cout << "~A(). length = " << mLength << ".";

    if (mData != NULL) {
            std::cout << " Deleting resource.";
        delete[] mData;  // Delete the resource.
    }

    std::cout << std::endl;
    }

    // Copy constructor.
    A(const A& other)
        : mLength(other.mLength), mData(new int[other.mLength])
    {
    std::cout << "A(const A&). length = "
        << other.mLength << ". Copying resource." << std::endl;

    std::copy(other.mData, other.mData + mLength, mData);
    }

    // Copy assignment operator.
    A& operator=(const A& other)
    {
    std::cout << "operator=(const A&). length = "
             << other.mLength << ". Copying resource." << std::endl;

    if (this != &other) {
        delete[] mData;  // Free the existing resource.
        mLength = other.mLength;
            mData = new int[mLength];
            std::copy(other.mData, other.mData + mLength, mData);
    }
    return *this;
    }

    // Move constructor.
    A(A&& other) : mData(NULL), mLength(0)
    {
        std::cout << "A(A&&). length = " 
             << other.mLength << ". Moving resource.\n";

        // Copy the data pointer and its length from the 
        // source object.
        mData = other.mData;
        mLength = other.mLength;

        // Release the data pointer from the source object so that
        // the destructor does not free the memory multiple times.
        other.mData = NULL;
        other.mLength = 0;
    }

    // Move assignment operator.
    A& operator=(A&& other)
    {
        std::cout << "operator=(A&&). length = " 
             << other.mLength << "." << std::endl;

        if (this != &other) {
          // Free the existing resource.
          delete[] mData;

          // Copy the data pointer and its length from the 
          // source object.
          mData = other.mData;
          mLength = other.mLength;

          // Release the data pointer from the source object so that
          // the destructor does not free the memory multiple times.
          other.mData = NULL;
          other.mLength = 0;
       }
       return *this;
    }

    // Retrieves the length of the data resource.
    size_t Length() const
    {
        return mLength;
    }

private:
    size_t mLength; // The length of the resource.
    int* mData;     // The resource.
};

移动构造函数

语法:

A(A&& other) noexcept    // C++11 - specifying non-exception throwing functions
{
  mData =  other.mData;  // shallow copy or referential copy
  other.mData = nullptr;
}

最主要的是没有用到新的资源,是移动而不是拷贝。 假设一个地址指向了一个有一百万个int元素的数组,使用move构造函数,我们没有创造什么,所以代价很低。

// Move constructor.
A(A&& other) : mData(NULL), mLength(0)
{
    // Copy the data pointer and its length from the 
    // source object.
    mData = other.mData;
    mLength = other.mLength;

    // Release the data pointer from the source object so that
    // the destructor does not free the memory multiple times.
    other.mData = NULL;
    other.mLength = 0;
}

移动比拷贝更快!!!

移动赋值运算符

语法:

A& operator=(A&& other) noexcept
{
  mData =  other.mData;
  other.mData = nullptr;
  return *this;
}

工作流程这样的:Google上这么说的:

Release any resources that this currently owns. Pilfer other’s resource. Set other to a default state. Return this.

// Move assignment operator.
A& operator=(A&& other)
{
    std::cout << "operator=(A&&). length = " 
             << other.mLength << "." << std::endl;

    if (this != &other) {
      // Free the existing resource.
      delete[] mData;

      // Copy the data pointer and its length from the 
      // source object.
      mData = other.mData;
      mLength = other.mLength;

      // Release the data pointer from the source object so that
      // the destructor does not free the memory multiple times.
      other.mData = NULL;
      other.mLength = 0;
   }
   return *this;
}

让我们看几个move带来的好处吧! vector众所周知,C++11后对vector也进行了一些优化。例如vector::push_back()被定义为了两种版本的重载,一个是cosnt T&左值作为参数,一个是T&&右值作为参数。例如下面的代码:

std::vector<A> v;
v.push_back(A(25));
v.push_back(A(75));

上面两个push_back()都会调用push_back(T&&)版本,因为他们的参数为右值。这样提高了效率。

而当参数为左值的时候,会调用push_back(const T&) 。

#include <vector>

int main()
{
    std::vector<A> v;
    A aObj(25);       // lvalue
    v.push_back(aObj);  // push_back(const T&)
}

但事实我们可以使用 static_cast进行强制:

// calls push_back(T&&)
v.push_back(static_cast<A&&>(aObj));

我们可以使用std::move完成上面的任务:

v.push_back(std::move(aObj));  //calls push_back(T&&)

似乎push_back(T&&)永远是最佳选择,但是一定要记住: push_back(T&&) 使得参数为空。如果我们想要保留参数的值,我们这个时候需要使用拷贝,而不是移动。

最后写一个例子,看看如何使用move来交换两个对象:

#include <iostream>
using namespace std;

class A
{
public:
    // constructor
    explicit A(size_t length)
            : mLength(length), mData(new int[length]) {}

    // move constructor
    A(A&& other)
    {
        cout<<"右值引用"<<endl;
        mData = other.mData;
        mLength = other.mLength;
        other.mData = nullptr;
        other.mLength = 0;
    }

    // move assignment
    A& operator=(A&& other) noexcept
    {
        cout<<"move"<<endl;
        mData =  other.mData;
        mLength = other.mLength;
        other.mData = nullptr;
        other.mLength = 0;
        return *this;
    }

    size_t getLength() { return mLength; }


    void swap(A& other)
    {
        cout<<other.getLength()<<endl;//11
        A temp = move(other);
        cout<<other.getLength()<<endl;//0
        cout<<temp.getLength()<<endl;//11
        cout<<this->getLength()<<endl;//22
        other = move(*this);
        cout<<other.getLength()<<endl;//22
        cout<<this->getLength()<<endl;//0
        cout<<"first"<<endl;
        cout<<temp.getLength()<<endl;//11
        *this = move(temp);
        cout<<temp.getLength()<<endl;//0
        cout<<this->getLength()<<endl;//11
        cout<<"second"<<endl;
    }

    int* get_mData() { return mData; }

private:
    int *mData;
    size_t mLength;
};

int main()
{
    A a(11), b(22);
    cout << a.getLength() << ' ' << b.getLength() << endl;
    cout << a.get_mData() << ' ' << b.get_mData() << endl;
    swap(a,b);
    cout << a.getLength() << ' ' << b.getLength() << endl;
    cout << a.get_mData() << ' ' << b.get_mData() << endl;
    a.swap(b);
    cout << a.getLength() << ' ' << b.getLength() << endl;
    cout << a.get_mData() << ' ' << b.get_mData() << endl;
    return 0;
}

运行结果:

11 22
0xb11420 0xb11790
鍙冲€煎紩鐢
move
move
22 11
0xb11790 0xb11420
11
鍙冲€煎紩鐢
0
11
22
move
22
0
first
11
move
0
11
second
11 22
0xb11420 0xb11790

自己在使用过程中发现,新变量的地址和旧变量是不一致的。 move实际上是将旧内存复制到了新内存,然后在旧内存的地方开头写入0x00(字符串是这样的,简单int不受影响),使其失效。

所以move之前一定要确认之前的东西不用了,保证他被销毁了没人再使用了。

下面这份测试代码中,有把数据从一个map move到另一个map的操作,并且之后还访问之前的map,这是不安全的,最好move完把之前的销毁。

#include <iostream>
#include <string>
#include <map>
#include <utility>
#include <vector>
#include <stdio.h>

using namespace std;

// check if stuct have move constructor
struct StructMove {
    StructMove(){};
    StructMove(int n): _n(n) { cout<<"construct "<<n<<endl;};
    int _n;
};

class ClassMove {
public:
    ClassMove():_n(0){cout<<"default construct"<<endl;};
    ClassMove(int n):_n(n){cout<<"construct"<<endl;};
    // Copy assignment operator is implicitly deleted because 'ClassMove' has a user-declared move constructor
    // 当你自定义了copy construct的时候,记得同时写一下移动赋值运算符的重载
    ClassMove(const ClassMove& classMove) {_n=classMove._n; cout<<"copy construct"<<endl;}
    // ClassMove(const ClassMove&& classMove) {_n=classMove._n; cout<<"move construct"<<endl;}
    // Move constructor.
    ClassMove(ClassMove&& other) noexcept { *this = std::move(other); }

    //移动赋值运算符重载
    ClassMove& operator = (ClassMove&& other) {
        this->_n = other._n;
        return *this;
    }
    int _n;
};
int main() {
    cout << "structMove" << endl;
    map<int, StructMove> m1;
    map<int, StructMove> m2;
    m1[0] = StructMove(10);
    printf("%x\n", &m1[0]);
    printf("%x\n", &(m1[0]._n));
    m2[0] = std::move(m1[0]);
    cout << m2[0]._n << endl;
    printf("%x\n", &m2[0]);
    printf("%x\n", &(m2[0]._n));
    cout << m1[0]._n << endl;
    cout << m2[0]._n << endl;
    cout << "end structMove" << endl;
    cout << endl;

    cout << "start ClassMove" << endl;
    map<int, ClassMove> mc1;
    map<int, ClassMove> mc2;
    mc1[0] = ClassMove(11);
    printf("%x\n", &(mc1[0]._n));
    mc2[1] = std::move(mc1[0]);
    cout << mc1[0]._n << endl;
    cout << mc2[1]._n << endl;
    printf("%x\n", &(mc1[0]._n));
    printf("%x\n", &(mc2[1]._n));
    cout << "end ClassMove" << endl;
    cout << endl;

    cout << "test base string" << endl;
    std::string str = "Hello";
    printf("%x\n", &(str[0]));
    printf("%x\n", &(str));
    std::vector<std::string> v;
    //调用常规的拷贝构造函数,新建字符数组,拷贝数据
    v.push_back(str);
    //调用移动构造函数,掏空str,掏空后,最好不要使用str
    v.push_back(std::move(str));
    cout << "*" << v[0] << "*" << v[1] << "*" << str << "*" << endl;
    printf("%x\n", &(v[0]));
    printf("%x\n", &(v[0][0]));
    printf("%x\n", &(v[1]));
    printf("%x\n", &(v[1][0]));
    cout << "end test base string" << endl;
    cout << endl;

    map<int, string> ms1;
    ms1[1] = "abc";
    printf("%x\n", &(ms1[1]));
    map<int, string> ms2;
    ms2[2] = std::move(ms1[1]);
    cout << ms1[1] << endl;
    cout << ms2[2] << endl;
    printf("%x\n", &(ms2[2]));

    return 0;
}

文章目录