C++11 Move semantics (移动语义)

C++11加入了右值引用(value reference)的概念(用&&标识),用来区分对左值和右值的引用。左值就是一个有名字的对象,而右值则是一个无名对象(临时对象)move语义允许修改右值(以前右值被看作是不可修改的,等同于const T&类型)。c++11中所有的值必须是左值、右值、将亡值之一。

即:在使用右值引用的时候,因为右值是一个无名的临时对象,对于对象的移动创建和移动复制操作,就可以不用重新申请内存空间,直接利用这个临时对象的内存空间和空间内的值,完成上述的操作。节约的新对象的内存分配,内存拷贝,和临时对象的内存释放。

一个有名字的对象就是左值。为了再把它变为右值引用(以便调用move constructor)必须使用std::move。标准库文件中 std::move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者内存拷贝。被转化的左值,其生命周围并没有随着左右值得转化而改变。 std::move基本等同于一个类型转换:static_cast(lvalue);

标准库头文件中提供了三个模板类:is_rvalue_reference、is_lvalue_reference、is_reference判断是否是右值引用、左值引用、是否是引用类型。还有一些模板可以帮助判断是否可移动is_move_constructible、is_trivially_move_constructible、is_nothrow_move_constructible。

用一个std::move_if_noexcept模板函数替代move函数。可以在类的移动狗制造函数没有noexcept关键字修饰时返回一个左值引用从而使变量可以使用拷贝语义,而在类的移动构造函数有noexcept关键字时,返回一个右值引用,从而是变量可以使用移动语义。

完美转发:在函数模板中,完全依照模板的参数类型,将参数传递给函数模板中调用的另一个函数

template

viod IamForwording(T t){      IrunCodeActually (t);     }

注意这种情况,会增加一次参数的拷贝,并且如果转发函数只能接收有一部分类型,那对执行函数的参数就会造成限制。

c++11中引用折叠:将复杂的位置表达式折叠为已知的简单表达式。如下表:

C++11 Move semantics (移动语义)_第1张图片

std::forward可以保存参数的左值或右值特性。

template

void IamForwording(T t) {     IrinCodeActually(forword(t));   }

C++的class或者struct以前都有一些隐含的成员函数:默认构造函数(仅当没有显示定义任何其他构造函数时才存在),拷贝构造函数,析构函数还有拷贝赋值操作符。拷贝构造函数和拷贝赋值操作符提供bit-wise的拷贝(浅拷贝),也就是逐个bit拷贝对象。也就是说,如果你有一个类包含指向其他对象的指针,拷贝时只会拷贝指针的值而不会管指向的对象。在某些情况下这种做法是没问题的,但在很多情况下,实际上你需要的是深拷贝,也就是说你希望拷贝指针所指向的对象。而不是拷贝指针的值。这种情况下,你需要显示地提供拷贝构造函数与拷贝赋值操作符来进行深拷贝如果你用来初始化或拷贝的源对象是个右值(临时对象)会怎么样呢?你仍然需要拷贝它的值,但随后很快右值就会被释放。这意味着产生了额外的操作开销,包括原本并不需要的空间分配以及内存拷贝。现在说说move constructormove assignment operator。这两个函数接收T&&类型的参数,也就是一个右值。在这种情况下,它们可以修改右值对象,例如“偷走”它们内部指针所指向的对象。举个例子,一个容器的实现(例如vector或者queue)可能包含一个指向元素数组的指针。当用一个临时对象初始化一个对象时,我们不需要分配另一个数组,从临时对象中把值复制过来,然后在临时对象析构时释放它的内存。我们只需要将指向数组内存的指针值复制过来,由此节约了一次内存分配,一次元数组的复制以及后来的内存释放。以下代码实现了一个简易的buffer。这个buffer有一个成员记录buffer名称(为了便于以下的说明),一个指针(封装在unique_ptr中)指向元素为T类型的数组,还有一个记录数组长度的变量。

template <typename T>

class Buffer

{

std::string _name;

size_t _size;

std::unique_ptr _buffer;

public:

// default constructor

Buffer():

_size(16),

_buffer(new T[16])

{}

// constructor

Buffer(const std::string& name, size_t size):

_name(name),

_size(size),

_buffer(new T[size])

{}

// copy constructor

Buffer(const Buffer& copy):

_name(copy._name),

_size(copy._size),

_buffer(new T[copy._size])

{

T* source = copy._buffer.get();

T* dest = _buffer.get();

std::copy(source, source + copy._size, dest);

}

// copy assignment operator

Buffer& operator=(const Buffer& copy)

{

if(this != ©)

{

_name = copy._name;

if(_size != copy._size)

{

_buffer = nullptr;

_size = copy._size;

_buffer = _size > 0 > new T[_size] : nullptr;

}

T* source = copy._buffer.get();

T* dest = _buffer.get();

std::copy(source, source + copy._size, dest);

}

return *this;

}

// move constructor

Buffer(Buffer&& temp):

_name(std::move(temp._name)),

_size(temp._size),

_buffer(std::move(temp._buffer))

{

temp._buffer = nullptr;

temp._size = 0;

}

// move assignment operator

Buffer& operator=(Buffer&& temp)

{

assert(this != &temp); // assert if this is not a temporary

_buffer = nullptr;

_size = temp._size;

_buffer = std::move(temp._buffer);

_name = std::move(temp._name);

temp._buffer = nullptr;

temp._size = 0;

return *this;

}

};

template <typename T>

Buffer getBuffer(const std::string& name)

{

Buffer b(name, 128);

return b;

}

int main(){

Buffer<int> b1;

Buffer<int> b2("buf2", 64);

Buffer<int> b3 = b2;

Buffer<int> b4 = getBuffer<int>("buf4");

b1 = getBuffer<int>("buf5");

return 0;

}

默认的copy constructor以及copy assignment operator大家应该很熟悉了。C++11中新增的是move constructor以及move assignment operator,这两个函数根据上文所描述的move语义实现。如果你运行这段代码,你就会发现b4构造时,move constructor会被调用。同样,对b1赋值时,move assignment operator会被调用。原因就在于getBuffer()的返回值是一个临时对象——也就是右值。你也许注意到了,move constuctor中当我们初始化变量name和指向buffer的指针时,我们使用了std::move。name实际上是一个string,std::string实现了move语义。std::unique_ptr也一样。但是如果我们写_name(temp._name),那么copy constructor将会被调用。不过对于_buffer来说不能这么写,因为std::unique_ptr没有copy constructor。但为什么std::string的move constructor此时没有被调到呢?这是因为虽然我们使用一个右值调用了Buffer的move constructor,但在这个构造函数内,它实际上是个左值。为什么?因为它是有名字的——“temp”。一个有名字的对象就是左值。为了再把它变为右值(以便调用move constructor)必须使用std::move。这个函数仅仅是把一个左值引用变为一个右值引用。更新:虽然这个例子是为了说明如何实现move constructor以及move assignment operator,但具体的实现方式并不是唯一的。某同学提供了另一种可能的实现。为了方便查看,我把它也列在下面:

template <typename T>

class Buffer

{

std::string _name;

size_t _size;

std::unique_ptr _buffer;

public:

// constructor

Buffer(const std::string& name = "", size_t size = 16):

_name(name),

_size(size),

_buffer(size? new T[size] : nullptr)

{}

// copy constructor

Buffer(const Buffer& copy):

_name(copy._name),

_size(copy._size),

_buffer(copy._size? new T[copy._size] : nullptr)

{

T* source = copy._buffer.get();

T* dest = _buffer.get();

std::copy(source, source + copy._size, dest);

}

// copy assignment operator

Buffer& operator=(Buffer copy)

{

swap(*this, copy);

return *this;

}

// move constructor

Buffer(Buffer&& temp):Buffer()

{

swap(*this, temp);

}

friend void swap(Buffer& first, Buffer& second) noexcept

{

using std::swap;

swap(first._name , second._name);

swap(first._size , second._size);

swap(first._buffer, second._buffer);

}

};

 

 如果一个表达式是函数调用或重载运算符表达式,且其返回类型为右值引用,例如 std::move(x),那么这个表达式是xvalue表达式

首先,如果要返回一个对象,肯定是要在栈上面预留内存空间的,所以这个对象是有身份的。第二,返回类型是右值引用,所以它会让使用这个表达式的语句呈现移动语义,所以是可移动的

#include

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

#include

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

v.push_back(A(25));

v.push_back(A(75));

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

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

#include

int main()

{

    std::vector v;

    A aObj(25);       // lvalue

    v.push_back(aObj);  // push_back(const T&)

}

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

// calls push_back(T&&)

v.push_back(static_cast(aObj));

 

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

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

似乎push_back(T&&)永远是最佳选择,但是一定要记住:

push_back(T&&) 使得参数为空。如果我们想要保留参数的值,我们这个时候需要使用拷贝,而不是移动。

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

#include

using namespace std;

class A

{

  public:

    // constructor

    explicit A(size_t length)

        : mLength(length), mData(new int[length]) {}

 

    // move constructor

    A(A&& other)

    {

      mData = other.mData;

      mLength = other.mLength;

      other.mData = nullptr;

      other.mLength = 0;

    }

 

    // move assignment

    A& operator=(A&& other) noexcept

    {

      mData =  other.mData;

      mLength = other.mLength;

      other.mData = nullptr;

      other.mLength = 0;

      return *this;

    }

 

    size_t getLength() { return mLength; }

    void swap(A& other)

    {

      A temp = move(other);

      other = move(*this);

      *this = move(temp);

    }

    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;

  return 0;

}

你可能感兴趣的:(C++,移动语义,c++11)