C++移动构造函数和移动赋值运算符详解

先来看一个 NumberArray 类程序:
//overload2.h中
class NumberArray
{
    private:
        double *aPtr;
        int arraySize;
    public:
        //复制赋值和复制构造函数
        NumberArray& operator=(const NumberArray &right);
        NumberArray(const NumberArray &);
       
        //默认构造函数和常规构造函数
        NumberArray();
        NumberArray(int size, double value);
       
        //析构函数
        〜NumberArray(〉 { if(arraySize > 0) delete [ ] aPtr; }
       
        void print() const;
        void seValue(double value);
}
该类的每个对象都拥有资源,也就是一个指向动态分配内存的指针。像这样的类需要程序员定义的复制构造函数和重载的复制赋值运算符,这些都是很有必要的,如果没有它们,那么按成员复制操作时将出现对资源的无意共享从而导致发送错误。

C++ 11 开始,类可以定义一个移动构造函数和一个移动赋值运算符。要更好地理解这些概念,则需要仔细研究析构函数、复制构造函数和复制赋值运算符的操作。为了帮助监控这些函数被调用时所发生的事情,可以修改 NumberArray 析构函数和所有的构造函数,使它们包含一个打印语句,跟踪这些函数的执行情况。

例如,可以按以下方式修改复制构造函数:
NumberArray::NumberArray(const NumberArray &obj)
{
    cout <<"Copy constructor running\n";
    arraySize = obj.arraySize;
    aPtr = new double[arraySize];
    for(int index = 0; index < arraySize; index++)
    {
        aPtr[index] = obj.aPtr[index];
    }
}
同样地,也可以将以下语句添加到析构函数、其他构造函数以及复制赋值运算符:
cout <<"Destructor running\n";
cout <<"Default constructor running\n";
cout <<"Regular constructor running\n";
cout <<"Assignment operator running\n";
下面的程序演示了复制构造函数和复制赋值运算符的工作方式。
// This program demonstrates the copy constructor and the
// copy assignment operator for the NumberArray class. #include <iostream>
#include "overload2.h"
using namespace std;

//Function Prototype
NumberArray makeArray();

int main()
{
    NumberArray first;
    first = makeArray();
    NumberArray second = makeArray();
    cout << endl << "The objectTs data is ";
    first.print();
    cout << endl;
    return 0;
}

//Creates a local object and returns it by value.
NumberArray makeArray()
{
    NumberArray nArr(5, 10.5);
    return nArr;
}
程序输出结果:

(1) Default constructor running
(2) Regular constructor running
(3) Copy constructor running
(4) Destructor running
(5) Copy Assignment operator running
(6) Destructor running
(7) Regular constructor running
(8) Copy constructor running
(9) Destructor running
(10) The objecfs data is 10.5010.5010.5010.5010.50
(11) Destructor running
(12) Destructor running

上述程序输出结果中的行编号并不是由程序生成的,这里添加它们只是为了方便讨论:
  • 第(1)行中的输出是由程序第 11 行中的默认构造函数生成的;
  • 第(2)行是由程序 makeArray() 函数创建局部对象时生成的;
  • 第(3)行的输出是当复制构造函数被调用,复制局部对象并创建从函数返回的临时对象时生成的;
  • 此时,局部对象被销毁,于是产生了第(4)行的输出;
  • 接下来,程序的第 12 行执行,调用复制赋值运算符并生成了第(5)行的输出;
  • 在赋值完成之后,临时对象被销毁,于是产生了第(6)行的输出;
  • 第(7)行的输出是在第 2 次调用 makeArray() 时通过创建局部对象生成的;
  • 返回的对象被直接复制到由复制构造函数第 2 次创建的对象中,由此产生了第(8)行的输出;
  • 此后函数中的局部对象被销毁,产生了第(9)行的输出;
  • 当在main函数的末尾销毁了 first 和 second 对象时,即产生了第(11)行和第(12)行的输出。

注意,有些编译器可能会进行优化,取消对某些构造函数的调用。

这里特别要关注的是程序的第 12 行:

first = makeArray();

它调用了复制赋值运算符以复制临时对象 makeArray(),如输出结果中的第(5)行所示。复制赋值运算符将删除 first 中的 aPtr 数组,分配另外一个数组,其大小和临时对象中的大小一样,然后将临时数组中的值复制到 first 的 aPtr 数组中。

复制赋值运算符将努力避免在 first 和临时对象之间共享指针,但是就在这以后,临时对象被销毁,其 aPtr 数组也被删除。隐藏在移动赋值运算符后面的想法就是,通过让被赋值的对象与临时对象交换资源,从而避免以上所有工作。

釆用这种方法之后,当临时对象被销毁时,它删除的是之前被 first 所占用的内存,而first则避免了复制以前在临时aPtr数组中的元素,因为这些元素现在已经属于它的了。

NumberArray 类的移动赋值运算符编写方法如下所示。为了简化代码,这里使用了一个库函数 swap 来交换两个内存位置的内容。swap 函数是在 <algorithm> 头文件中声明的。
NumberArray& NumberArray::operator=(NumberArray&& right)
{
    if (this != &right)
    {
        swap(arraySize, right.arraySize);
        swap(aPtr, right.aPtr);
    }
    return *this;
}
请注意,该函数的原型和复制赋值的原型类似,但是移动赋值釆用了右值引用作为形参。这是因为移动赋值应该仅在赋值的来源是一个临时对象时才执行。还需要注意的是,移动赋值的形参不能是 const,这是因为它需要通过修改对象 "移动" 资源。

移动赋值是在 C++11 中引入的,它明显比复制赋值高效得多,并且应该仅在赋值的来源是一个临时对象时才使用。此外还有一个移动构造函数,当创建一个新对象,并且新对象初始化的值来源于一个临时对象时,即可使用该构造函数。

与移动赋值一样,移动构造函数不必复制资源,而是直接从临时对象那里“窃取”资源。以下是 NumberArray 类的移动构造函数。在此说明一下,该构造函数的形参是一个右值引用,表示该形参是一个临时对象。
NumberArray::NumberArray(NumberArray && temp)
{
    //从temp对象中“窃取”资源
    this->arraySize = temp.arraySize;
    this->aPtr = temp.aPtr;
    //将temp放置到安全状态以防止其析构函数运行
    temp.arraySize = 0;
    temp.aPtr = nullptr;
}
请注意,移动构造函数的形参也不能是 const。此外,作为移动构造函数获取资源来源 的临时对象必须放置到安全状态,使得其析构函数可以正常运行而不会导致出错。

NumberArray 类的移动操作的实现可以在如下所示的 overload3.h 和 overload3.cpp 文件中找到:
//overload3.h 的内容
#include <iostream>
using namespace std;

class NumberArray
{
    private:
        double *aPtr;
        int arraySize;
    public:
        //Copy assignment and copy constructor
        NumberArrays operator=(constNumberArray &right);
        NumberArray(constNumberArray &);

        //Default constructor and Regular constructor NumberArray;
        NumberArray(int size, double value);

        //Move Assignment and Move Constructor NumberArrays
        operator=(NumberArray &&);
        NumberArray (NumberArray &&);
       
        // Destructor
        〜NumberArray();
       
        void print () const;
        void setValue(double value);
};
//overload3.cpp 的内容
#include <iostream>
#include "overload3.h"
using namespace std;
NumberArray&NumberArray::operator=(const NumberArray&right) {
    cout <<"Copy Assignment operator running\n";
    if (this != &right)
    {
        if (arraySize > 0)
        {
            delete [ ] aPtr;
        }
        arraySize = right.arraySize;
        aPtr = new double[arraySize];
        for (int index = 0; index < arraySize; index++) {
            aPtr[index] = right.aPtr[index];
        }
    }
    return *this;
}

NumberArray::NumberArray(const NumberArray&obj)
{
    cout <<"Copy constructor running\n";
    arraySize = obj.arraySize;
    aPtr = new double[arraySize];
    for (int index = 0; index < arraySize; index++)
    {
        aPtr[index] = obj.aPtr[index];
    }
}
NumberArray::NumberArray(int size1, double value)
{
    cout << "Regular constructor running\n"; arraySize = Size1;
    aPtr = new double[arraySize];
    setValue(value);
}
NumberArray::NumberArray()
{
    cout <<"Default constructor running\n";
    arraySize = 2;
    aPtr = new double[arraySize];
    setValue (0.0);
}
void NumberArray::setValue(double value)
{
    for (int index = 0; index < arraySize; index++)
    {
        aPtr[index] = value;
    }
}
void NumberArray::print()const
{
    for (int index = 0; index < arraySize; index++)
    {
        cout << aPtr [index] = " ";
    }
}
NumberArray::〜NumberArray()
{
    cout <<"Destructor running\n";
    if (arraySize > 0)
    {
        delete[] aPtr;
    }
}
NumberArray &NumberArray::operator(NumberArray&& right)
{
    cout << "Move assignment is running\n";
    if (this != &right)
    {
        swap(arraySize, right.arraySize);
        swap(aPtr, right.aPtr);
    }
    return *this;
}
NumberArray::NumberArray(NumberArray && temp)
{
    //从temp对象中“窃取”资源
    this->arraySize = temp.arraySize;
    this->aPtr = temp.aPtr;
    //将temp放置到安全状态
    //以防止其析构函数运行
    temp.arraySize = 0;
    temp.aPtr = nullptr;
}
下面的程序中演示了这些操作:
// This program demonstrates move constructor the move assignment operator.
#include <iostream>
#include "Toverload3.h"
using namespace std;

NumberArray makeArray();// Prototype

int main()
{
    NumberArray first;
    first = makeArray();
    NumberArray second = makeArray();
    cout << endl << "The object's data is ";
    first.print();
    cout << endl;
    return 0;
}
NumberArray makeArray()
{
    NumberArray nArr(5,10.5);
    return nArr;
}
程序输出结果:

(1) Default constructor running
(2) Regular constructor running
(3) Destructor running
(4) Move assignment is running
(5) Destructor running
(6) Regular constructor running
(7) Destructor running
(8) The objectTs data is10.5010.5010.5010.5010.50
(9) Destructor running
(10) Destructor running

通过检查程序输出结果的第(4)行可知,当所赋值的来源是一个临时对象时(参见程序的第 11 行代码),即可调用移动赋值函数。程序的第 12 行使用了一个临时对象来初始化一个 NumberArray 对象,这样就导致移动构造函数被调用。但是,在程序输出结果中并没有相关输出记录,这是因为编译器有些时候会使用优化技术避免调用复制或移动构造函数。

编译器使用移动操作的时机

与复制构造函数以及赋值运算符一样,移动操作也是在合适的时候才被编译器调用。

特别是,编译器将在以下时机使用移动操作。
  1. 函数通过值返回结果。
  2. 对象被赋值并且右侧是一个临时对象。
  3. 对象被使用一个临时对象进行初始化。

虽然绝大多数移动操作都被用于从一个临时对象中转移资源,但也并不总是这样。

例如,有一个 unique_ptr 类,某个 unique_ptr 可能需要将它管理的对象转移到另外一个 unique_ptr 对象。如果这个源目标并不是一个临时对象,那么编译器并不会告诉它自己说,这里应该使用移动操作。所以,在这种情况下,可以使用 std::move() 库函数。该函数的效果就是让它的实参看起来像是一个右值,并且允许移动它。