Chapter 13 Copy Control

正如前面所看到过的,每一个类定义了一个新的类型并且也定义了它相关的操作。在本章,我们学习的是类如何控制copy,assign,move,destroy。类通过copy constructor,move constructor , copy assignment operator,move-assignment operator,destructor来实现这些功能。

Copy, Assign, and Destroy

copy constructor是一个只有一个参数并且这个参数指向当前类的对象的一个引用的构造函数。如下:

class Foo {
public:
    Foo(); // default constructor
    Foo(const Foo&); // copy constructor
    // ...
};

至于为什么是引用我们会在后面说到,大多数情况下copy constructor应该接收一个const引用,但是它也允许non const的引用,copy constructor在许多地方被implicitly地使用,所以它不能被explicit修饰。

The Synthesized Copy Constructor

默认情况下的copy constructor执行的是逐个成员的复制,被复制对象的每一个成员(出了static,static并不是属于对象的)都复制给新的对象。成员的类型决定了复制是怎么样的,对于类来说,它执行的是这个类的copy constructor,对于内置类型来说,就是直接复制。虽然,我们不能直接复制数组,但是默认的copy constructor会将数组的每一个元素都复制。

测试代码如下:

Sales_data::Sales_data(const Sales_data &orig):
    bookNo(orig.bookNo), // uses the string copy constructor
    units_sold(orig.units_sold), // copies orig.units_sold
    revenue(orig.revenue) // copies orig.revenue
{ }

Copy Initialization

direct initialization是让编译器去选择合适的方法来创建对象,copy initialization则是告诉编译器将=操作符右边的数据复制给新创建的对象。Copy initialization在大多数情况下都是使用copy constructor的,但是有时候会使用move constructor,不过在目前我们只需要知道copy initialization会使用两者之一。

copy initialization 发生在如下三种情况:

  1. 一个对象作为argument传递传给一个非引用类型的parameter
  2. 一个对象作为返回值,且函数的返回值类型是非引用类型
  3. 使用大括号来初始化一个数组内的数据,或者是aggregate class的成员

Parameters and Return Values

参数和返回值在非引用类型的情况下才会调用copy constructor,这也就说明了为什么copy constructor的参数只能是引用类型。如果是值类型,那么调用copy constructor的时候,又需要调用copy constructor,是一个无法结束的递归过程。

The Compiler Can Bypass the Copy Constructor

在copy initialization期间,编译器被允许可以跳过copy constructor,而直接创建对象。例子如下:

string null_book = "9-999-99999-9"; // copy initialization
string null_book("9-999-99999-9"); // compiler omits the copy constructor

上述两者等价,虽然编译器可以不调用copy constructor,但是copy constructor 必须存在,并且

The Copy-Assignment Operator

类也控制着它的赋值操作是怎么样的,如下就是使用=运算符。

Sales_data trans, accum;
trans = accum; // uses the Sales_data copy-assignment operato

默认的=运算符执行的是逐个成员的赋值。

Introducing Overloaded Assignment

运算符重载就是重新定义运算符的功能的函数,它的形式以operator关键字开始,后面就是接收的参数以及返回值。运算符重载的参数取决于运算符的操作对象有几个。某一些操作符,比如说assignment,必须是成员函数,当一个operator是成员函数的时候,操作符左边的对象就是this,对于二元操作符来说,右边的操作对象就是作为parameter。重载=的形式如下:

Foo& operator=(const Foo&);     // const Foo & 就是等号右边的对象

为了和内置对象相一致,assignment通常返回的都是引用。而且,许多库函数要求存放在容器内的对象有一个返回引用类型的=操作符重载函数

Assignment operators ordinarily should return a reference to their left-hand operand

The Synthesized Copy-Assignment Operator

如果一个类没重载=运算符,那么编译器就会产生一个默认的。但是有些类不允许=运算符的使用,默认情况下的=运算符也是进行逐个成员的赋值,出了static成员,数组的话则是将每个元素都赋值给目标对象。

The Destructor

析构函数的作用就是将资源释放掉。析构函数以~开头,因为它不接收任何参数,所以析构函数不能被重载。

What a Destructor Does

在构造函数当中,先执行initialize list中的内容,然后再执行构造函数的代码,并且在初始化成员的时候,先出现的成员先被初始化,这个顺序并不是取决于initial list中的顺序。但是在析构函数中,函数体先执行,然后再释放成员变量。

析构函数没有像构造函数那样的initializer list来控制如何释放数据,如何释放数据取决于成员的类型,如果一个成员是某个类,那么会调用这个类的析构函数来释放该成员。

The implicit destruction of a member of built-in pointer type does not delete the object to which that pointer points.

对于指针来说,析构函数不会调用delete来释放,所以如果我们使用了裸指针,记得自己去delete。智能指针有析构函数,所以某个类如果使用了智能指针,这个指针会该类的析构函数中被释放。

When a Destructor Is Called

以下情况都会调用析构函数:

  1. 变量离开了一个scope
  2. 一个对象的成员当所包含它的对象被销毁的时候,也会调用它的析构函数。如A包含B,当A销毁时,B的析构函数也会被调用。
  3. 当容器或者数组被销毁的时候,其内部的成员也会被销毁。
  4. 当用delet去回收动态内存的时候,动态内存的里面的对象也被销毁。
  5. 临时对象会在整个语句执行完毕的时候被销毁。

The Synthesized Destructor

编译器提供了一个默认的析构函数,并且对于有一些类来说,默认的析构函数定义是为了不让这个类的对象被释放。默认的析构函数的空的函数体,成员变量的释放位于析构函数的函数体之后

如下是一个默认的析构函数:

class Sales_data {
public:
    // no work to do other than destroying the members, which happens automatically
    ~Sales_data() { }
    // other members as before
};

The Rule of Three/Five

虽然在很多情况下我们并不需要定义copy constructor,copy assignemnt, destructor,下面是一些指导性原则。

Classes That Need Destructors Need Copy and Assignment

如果需要重写析构函数,那么就需要重写copy constructor 和copy assignment。测试代码如下:

class HasPtr {
public:
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)), i(0) { }
    ~HasPtr() { delete ps; }
    // WRONG: HasPtr needs a copy constructor and copy-assignment operator
    // other members as before
};
HasPtr f(HasPtr hp) // HasPtr passed by value, so it is copied
{
    HasPtr ret = hp; // copies the given HasPtr
    // process ret
    return ret; // ret and hp are destroyed
}

HasPtr p("some values");
f(p); // when f completes, the memory to which p.ps points is freed
HasPtr q(p); // now both p and q point to invalid memory!

因为我们使用了动态内存,所以显然需要在析构函数当中使用delete释放。但是,因为默认的copy constuctor和copy assignment operator执行的都是逐个成员的复制,就会导致多个指针指向了相同的内存。当函数f执行完毕以后,hb和ret都会调用析构函数,出现了在同一个指针上delete了两次,这是未定义的行为。甚至,如果后续ret的返回值还会被使用,那么代码就出错了

If a class needs a destructor, it almost surely also needs the copy-assignment operator and a copy constructor.

Classes That Need Copy Need Assignment, and Vice Versa

原文给了一个例子说明重写了copy assignment 并不需要重写析构函数。

If a class needs a copy constructor, it almost surely needs a copy-assignment operator And vice versa—if the class needs an assignment operator, it almost surely needs a copy constructor as well. Nevertheless, needing either the copy constructor or the copy-assignment operator
does not (necessarily) indicate the need for a destructor

Using = default

我们可以显示的让编译器产生默认的析构函数和构造函数,以及copy constructor。

class Foo {
public:
    Foo() = default;
    Foo(const Foo &) = default;
    ~Foo() = default;
};

如果我们在类的内部使用= default声明这些函数,那么这些函数都是默认为inline的。当然也可以在函数的定义的时候使用= default,那么就不是inline了。

We can use = default only on member functions that have a synthesized version

Preventing Copies

虽然大多数的类应该定义copy constructor以及copy assignment oprator,但是有一些而言,定义这些操作是不恰当的,因此在这些去场景中,必须禁止这些函数。比如说iostream就禁止了复制,来避免多个对象读写同一个 IO 缓冲区。

Defining a Function as Deleted

在c++ 11中,我们可以避免复制,通过在copy constructor 和copy assignment oprator之后加上= delete,这样编译器就知道不要为它们产生默认的copy constructor和copy assigment oprator。

struct NoCopy {
    NoCopy() = default; // use the synthesized default constructor
    NoCopy(const NoCopy&) = delete; // no copy
    NoCopy &operator=(const NoCopy&) = delete; // no assignment
    ~NoCopy() = default; // use the synthesized destructor
    // other members
};

= default不同的是,= delete必须出现在首次函数声明的地方。这是因为,= default只是影响着编译器如何去产生代码,所以,直到编译器正式产生代码之前,= default位于何处并没有什么关系。但是,编译器需要预先知道是否需要产生某个函数,那么就可以在编译期间避免那些使用了这些被delete的函数的代码。

而且 我们只能将=default是用默认构造函数,析构函数,以及和复制相关的函数。但是可以将=delete用在任何地方,虽然主要是用在那些和复制相关的函数

The Destructor Should Not be a Deleted Member

我们不能够将析构函数设置为delete,不然的话编译器不会产生默认的析构函数,于是我们也不能释放内存。并且,编译器也不允许我们定义或者创建一个具有delete的析构函数的类型的临时变量。并且,一个类也不能将某个析构函数为delete的类作为成员。虽然不能直接创建具有delete的析构函数的类,但是仍然可以通过new来创建这样的对象,结果是,delete无法释放内存。

struct NoDtor {
    NoDtor() = default; // use the synthesized default constructor
    ~NoDtor() = delete; // we can't destroy objects of type NoDtor
};
NoDtor nd; // error: NoDtor destructor is deleted
NoDtor *p = new NoDtor(); // ok: but we can't delete p
delete p; // error: NoDtor destructor is deleted

It is not possible to define an object or delete a pointer to a dynamically allocated object of a type with a deleted destructor.

The Copy-Control Members May Be Synthesized as Deleted

在某些类当中,编译器所产生的默认的synthesized members是deleted function:

  1. 如果某个成员的析构函数是deleted或者private的,那么它的析构函数也是deleted
  2. 如果某个成员的copy constructor是deleted或者pivate的,那么它的copy constructor也是deleted。而且如果它的成员的析构函数是deleted或者private,也会导致这个类的copy constructor也是deleted的。
  3. 如果一个成员的copy-assignment operator是delete或者private的,或者类具有const或者reference引用,那么编译器为这个类产生的默认的copy-assignment operator是delete的
  4. The synthesized default constructor is defined as deleted if the class has a member with a deleted or inaccessible destructor; or has a reference member that does not have an in-class initializer (§ 2.6.1, p. 73); or has a const member whose type does not explicitly define a default constructor and that member does not have an in-class initializer.

因为析构函数是deleted或者private,进而会导致默认的构造函数和Copy constructor也是deleted,这是因为析构函数delete了,如果我们再去创建对象,那么会导致内存无法释放。

并且,如果一个类具有未初始化的const或者引用成员,那么也不能产生默认的构造函数,因为const成员一定要被显式的初始化。也因此,=运算符起不了作用,因为const成员无法被修改,默认的=运算符执行的是逐个成员的复制。测试代码如下:

class Foo {
private:
    const int num;
public:
    explicit Foo() = default;
};
int main() {
    Foo f;  //无法创建,因为num没有被初始化
}

还有一些其他原因会导致这些成员是被deleted,后面在介绍。

In essence, the copy-control members are synthesized as deleted when it is impossible to copy, assign, or destroy a member of the class.

private Copy Control

在新的标准之前,一些类通过将copy constructor和copy-assignment operator声明为private使得copy无法实现。下面代码中没有指明修饰权限的限定词就说明这默认是private的。

class PrivateCopy {
    // no access specifier; following members are private by default;
    // copy control is private and so is inaccessible to ordinary user code
    PrivateCopy(const PrivateCopy&);
    PrivateCopy &operator=(const PrivateCopy&);
    // other members
    public:
    PrivateCopy() = default; // use the synthesized default constructor
    ~PrivateCopy(); // users can define objects of this type but not copy them
};

虽然避免了外部代码进行复制相关的操作,但是类的成员变量以及friends还是可以执行复制,因此对copy constructor和copy-assignment operator只声明但是不定义。使用这些声明但是没有定义的函数会出现link time failure,于是就避免了成员方法使用copy的情况。

Classes that want to prevent copying should define their copy constructor and copy-assignment operators using = delete rather than making those members private

Copy Control and Resource Management

如果一个类需要管理不是位于类的内部的成员,这里说的其实就是那些分配在堆上的数据,必须定义和复制相关的成员,并且这些成员需要在析构函数中给回收。因为我们重写了析构函数,依照前面提到的准则,也需要重写和复制相关的函数。

那么我们首先要考虑的就是,我们的目的是什么,如果想让两个对象共享某个成员,那么就是指针形式的变量,如果是互相隔离的,那么就是值形式的。比如说,string就是一个Valuelike,而shared_ptr就是一个Pointer like。

Classes That Act Like Values

如果要让一个成员是valuelike,那么就需要对数据进行复制,为了实现这一目的,要求如下:

  1. copy constructor必须复制指针所指向的数据,而不是复制指针所指向的地址
  2. 析构函数要释放
  3. =操作符也需要复制数据,而不是复制指针所指向的地址。

如下代码演示了copy constructor的正确操作:

class HasPtr {
public:
    HasPtr(const std::string &s = std::string()):
        ps(new std::string(s)), i(0) { }
    // each HasPtr has its own copy of the string to which ps points
    HasPtr(const HasPtr &p):    // copy constructor,复制一个string
        ps(new std::string(*p.ps)), i(p.i) { }
    HasPtr& operator=(const HasPtr &);
    ~HasPtr() { delete ps; }
private:
    std::string *ps;
    int i;
};

Valuelike Copy-Assignment Operator

=运算符要做的事情和copy constructor差不多,不过要考虑到自己赋值给自己的情况。

HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    auto newp = new string(*rhs.ps); // copy the underlying string
    delete ps; // free the old memory
    ps = newp; // copy data from rhs into this object
    i = rhs.i;
    return *this; // return this object
}

Key Concept: Assignment Operators
There are two points to keep in mind when you write an assignment operator:
• Assignment operators must work correctly if an object is assigned to itself.
• Most assignment operators share work with the destructor and copy constructor.
A good pattern to use when you write an assignment operator is to first copy the right-hand operand into a local temporary. After the copy is done, it is safe to destroy the existing members of the left-hand operand. Once the left- hand operand is destroyed, copy the data from the temporary into the members of the left-hand operand.

下面代码演示了如果不创建临时变量存在的问题,delete ps直接将本身的ps内存释放,new string(*(rhs.ps));接着又去访问被释放的内存,这里出现了未定义的行为。

HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    delete ps; // frees the string to which this object points
    // if rhs and *this are the same object, we're copying from deleted memory!
    ps = new string(*(rhs.ps));
    i = rhs.i;
    return *this;
}

Defining Classes That Act Like Pointers

如果需要让多个对象共享数据,也就是pointer like,那么需要copy constructor和等号操作符赋值指针,而不是指针所指向的数据。在释放的时候,只有最后一个对象调用析构函数的时候才回收堆上的内存,就如同shared_ptr所作的那样,当然直接使用shared_ptr也是可以的。下面展示模仿shared_ptr的实现方法。

  1. 创建对象的时候,每一个构造函数都会创建一个计数器,用于记录多少个对象在共享,当只创建一个对象的时候,这个计数器为1。

  2. copy constructor不会创建一个新的counter,它只是复制原来的counter,并且增加计数器,表明还有其他对象在共享成员。

  3. 析构函数会递减counter,只有conter为0的时候,才释放所占据的内存。

  4. copy-assignment operator递增右操作符的counter,递减左操作符的counter,如果左边操作符的counter为0,那么copy-assignment operator的内部必须释放内存。

    这个例子有些不好理解,我没有理解为什么需要递减左边的counter。原因是考虑以下例子:

    F f1("hello");
    F f2("hello1");
    f1 = f2; 

    那么f1中的hello不在需要,所以需要被释放。

另外一个问题就是,如何在多个对象之间共享counter,普通的int类型肯定是不行的,因为这做不到共享,所以应该使用指针类型。如下:

class HasPtr {
public:
    // constructor allocates a new string and a new counter, which it sets to 1
    HasPtr(const std::string &s = std::string()):
         ps(new std::string(s)), i(0), use(new std::size_t(1)){}
    // copy constructor copies all three data members and increments the counter
    HasPtr(const HasPtr &p):
        ps(p.ps), i(p.i), use(p.use) { ++*use; }
    HasPtr& operator=(const HasPtr&);
    ~HasPtr();
private:
    std::string *ps;
    int i;
    std::size_t *use; // member to keep track of how many objects share *ps
};
HasPtr::~HasPtr()
{
    if (--*use == 0) { // if the reference count goes to 0
        delete ps; // delete the string
        delete use; // and the counter
    }
}
HasPtr& HasPtr::operator=(const HasPtr &rhs)
{
    ++*rhs.use; // increment the use count of the right-hand operand
    if (--*use == 0) { // then decrement this object's counter
        delete ps; // if no other users
        delete use; // free this object's allocated members
    }
    ps = rhs.ps; // copy data from rhs into this object
    i = rhs.i;
    use = rhs.use;
    return *this; // return this object
}

即使发生了自己赋值给自己,首先++*rhs.use;,然后--*use,最终会在析构函数中被释放。

Swap

出了前面提到的函数意外,许多类还会定义一个swap函数,swap对于那些对元素进行reorder的算法十分重要。如果某个类实现了自己的swap,那么就使用该类的swap,否则使用的就是库函数的swap。一般来说,swap的语句十分简单,就是两个assigment 和一个copy,如下:

HasPtr temp = v1; // make a temporary copy of the value of v1
v1 = v2; // assign the value of v2 to v1
v2 = temp; // assign the saved value of v1 to v2

但是因为我们对HasPtr重写了copy constructor和=运算符,所以这里都会发生数据的复制,这些复制是多余的。实际上我们只需要交换指针的地址即可:

string *temp = v1.ps; // make a temporary copy of the pointer in v1.ps
v1.ps = v2.ps; // assign the pointer in v2.ps to v1.ps
v2.ps = temp; // assign the saved pointer in v1.ps to v2.ps

Writing Our Own swap Function

实现我们自己的swap也十分简单,只需要调用默认的swap来交换成员即可,如下:

class HasPtr {
    friend void swap(HasPtr&, HasPtr&);
    // other members as in § 13.2.1 (p. 511)
};
inline
void swap(HasPtr &lhs, HasPtr &rhs)
{
    using std::swap;
    swap(lhs.ps, rhs.ps); // swap the pointers, not the string data
    swap(lhs.i, rhs.i); // swap the int members
}

Using swap in Assignment Operators

有些类通常使用swap来重写它们的=操作符,称为copy and swap,代码如下:

HasPtr& HasPtr::operator=(HasPtr rhs)
{
    // swap the contents of the left-hand operand with the local variable rhs
    swap(*this, rhs); // rhs now points to the memory this object had used
    return *this; // rhs is destroyed, which deletes the pointer in rhs
}

因为参数传的是值,所以rhs是=操作符右操作对象的copy,主要因为前面我们已经重写了HasPtr的copy constructor,是共享string的,且通过计数器来控制回收的过程,所以这里即使参数rhs被释放以后,也不会导致内部的string被释放。

Moving, Not Copying, Elements during Reallocation

前面在描述StrVec的设计,就不多赘述。这里主要考虑的场景是,存放string的空间不够了,需要重新申请空间,然后将已有的string复制,然后将老的空间上的string全部移除,显然效率十分低下

Move Constructors and std::move

避免这个问题主要有两个方法,一个是使用move constructor,另外一个是使用std::move。move constructor的意思是从所给出的对象中的资源移动到被创建的对象中 ,而且,原来的string并没有被释放

For string, we can imagine that each string has a pointer to an array of char. Presumably the string move constructor copies the pointer rather than allocating space for and copying the characters themselves.

第二个方法是使用move函数,定义在ultility头文件中。到目前,我们只需要知道调用move函数实际上会调用move constructor,并且因为某些原因,我们通常不会对move使用using declaration,使用的是std::move而不是move。有些内容略过,主要是如下:

alloc.construct(dest++, std::move(*elem++));

前面说过construct需要依赖于参数来决定调用哪个构造函数,所以这里的move的返回值传给collect,让它知道调用move constructor,进而避免调用copy constructor。

Moving Objects

新标准中最重要的特性就是move对象,而不是copy。move可以带来很大的性能提升,并且在有一些类当中,并不能copy,如IO和unique_ptr类,因为它们当中的有些资源不能被共享,但是可以被move。早期的 C++ 当中,并没有直接的方法来move对象,所以只能进行copy,性能开销比较大,存放在容器当中的类也必须是能够被复制的,在新标准中,这一切不复存在了。

The library containers, string, and shared_ptr classes support move as well as copy. The IO and unique_ptr classes can be moved but not copied

Rvalue References

为了支持move操作,引入了一种新的引用,称为右值引用,它必须和一个右值绑定在一起,使用&&来获得一个右值引用。右值引用的一个重要的特性,就是它是和一个马上就要被销毁的数据绑定。所以,我们将一个resources从右值引用移动到另外一个对象是没有问题的。

广义来说,左值表示的是一个对象的实体,而右值表示的是一个对象的值。int num = 1,num是左值,而1是右值。前面我们学过,不能将一个普通的引用和需要转换的数据类型,字面常量,或者是返回右值的表达式绑定,例子如下:

int &r1 = 3.14; //类型转换
int &r2 = 3; // 常量
int &r3 = foo(); // foo是一个右值的函数

但是可以却可以和右值引用相绑定,而且我们不能rvalue reference和lvalue相绑定:

int i = 42;
int &r = i; // ok: r refers to i
int &&rr = i; // error: cannot bind an rvalue reference to anlvalue
int &r2 = i * 42; // error: i * 42 is an rvalue
const int &r3 = i * 42; // ok: we can bind a reference to const to an rvalue
int &&rr2 = i * 42; // ok: bind rr2 to the result of the multiplication

返回左值引用的函数,赋值,subscript,dereference,prefix increment/decrement operators都是返回的是lvalues,可以和任何的左值引用像绑定。但是

Functions that return a nonreference type, along with the arithmetic, relational, bitwise, and postfix increment/decrement operators, all yield rvalues.

所以不能用左值引用和这些东西相绑定,除了右值引用和const的左值引用,这是因为const左值引用会创建临时变量。

Lvalues Persist; Rvalues Are Ephemeral

仔细观察左值和右值表达式,两者的主要区别lvalues和rvalues是lvalues具有持续性的状态,而右值通常是临时变量或者是字面常量。因为右值引用绑定临时变量,所以可以说:

  1. 所绑定的数据很快就会被销毁。
  2. 没有其他users在使用这些临时变量。

这些条件表明使用右值引用的函数可以很放心的take over resources from the object to which the reference refers.

Rvalue references refer to objects that are about to be destroyed. Hence, we can “steal” state from an object bound to an rvalue reference.

Variables Are Lvalues

一个变量也是一个左值,所以我们不能将右值引用同一个变量绑定在一起

int &&rr1 = 42; // ok: literals are rvalues
int &&rr2 = rr1; // error: the expression rr1 is an lvalue!

用前面的理论来描述就是,rr1并不是临时的对象,所以他应该是一个lvalue。

A variable is an lvalue; we cannot directly bind an rvalue reference to a variable even if that variable was defined as an rvalue reference type.

The Library move Function

虽然不可以直接将右值引用和一个左值绑定在一起,但是仍然可以通过强制类型转换将lvalue转为右值引用类型,或者是使用move函数。move函数的具体原理后面再说,但是目前只需要知道move返回的是一个与传给它的参数所绑定的右值引用。

int &&rr3 = std::move(rr1); // ok

It is essential to realize that the call to move promises that we do not intend to use rr1 again except to assign to it or to destroy it. After a call to move, we cannot make any assumptions about the value of the moved-from object.

也就是说,某个对象被move之后,就不要再使用了。并且要使用std::move来避免命名冲突。

Code that uses move should use std::move, not move. Doing so avoids potential name collisions.

Move Constructor and Move Assignment为了能够在自己定义的类型当中使用move 操作,那么就需要定义move constructor,以及 move-assignment operator,它们从所给的对象当中偷走数据,而不是复制。move constructor的参数是一个右值引用类型,并且如果包含其他参数,那么这些参数都需要具备默认值。除了会move resources以外,move constructor必须保证原先的对象处于一个被销毁也不会带来任何影响的状态,也就是说,当一个对象的数据被move之后,它不应该在指向这些已经被移除的数据,这些数据的管理交给了接收了这些数据的对象。如下就是一个StrVec的move constructor的例子:

StrVec::StrVec(StrVec &&s) noexcept // move won't throw anyexceptions
    // member initializers take over the resources in s
    :
    elements(s.elements),first_free(s.first_free), cap(s.cap)
    {
    // leave s in a state in which it is safe to run the destructor
     s.elements = s.first_free = s.cap = nullptr;
}

move constructor不会进行任何数据的复制,只是进行了几个指针的改变,并且还需要将原先对象的指针设置为nullptr,符合前面我们说过的,原先对象不应该指向被move的数据。最后,原先的对象还是会被释放,如果我们忽略了设置为nullptr的过程,那么就会造成新对象的数据被释放了。

Move Operations, Library Containers, and Exceptions

因为move 操作执行的只是stealing其他对象的元素,它本身不会分配数据,所以move操作一般来说不会抛出异常。如果我们编写一个move操作的时候,应该注明不会抛出异常,否则的话,library将会做一些额外的工作来处理move operation会出现的异常。

一个做法就是在构造函数之后noexcept加上,表明该函数不会抛异常。语法如下:


class StrVec {
public:
    StrVec(StrVec&&) noexcept; // move constructor
    // other members as before
};
StrVec::StrVec(StrVec &&s) noexcept : /* member initializers */
    { /* constructor body */ }

我们必须在函数的声明以及定义上都表示noexcept。关于为什么move constructor一定要是noexcept,暂时不理解,有空认真学习下。

Move-Assignment Operator

move-assignment operator做的事情和copy-assignment operator以及move constructor相类似,而且也应该是noexcept的,也要注意自己赋值给自己的情况。

StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
    // direct test for self-assignment
    if (this != &rhs) {
        free(); // free existing elements
        elements = rhs.elements; // take over resources from rhs
        first_free = rhs.first_free;
        cap = rhs.cap;
        // leave rhs in a destructible state
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
    return *this;
}

插入一下,operator=(StrVec &&rhs)会在哪里被使用呢?是在StrVec v = std::move(v1),实际上这里相当于v.operator=(std::move(v1)),而move()返回的是右值引用。

A Moved-from Object Must Be Destructible

move操作并不会导致原来的对象被销毁,虽然有些情况下原来的对象用完之后就销毁了。因此,原来的对象在被move之后必须处于一个稳定状态,即析构函数可以正常作用于它。这也就是前面我们在move operator和move constructor中将那些指针设置为nullptr的原因。

而且,move 操作必须保证这个对象仍然valid的,也就是这个对象仍然可以被赋值,或者在那些不依赖于当前的数据的操作中被使用。总的来说,程序不能够依赖于被move的对象。

After a move operation, the “moved-from” object must remain a valid, destructible object but users may make no assumptions about its value.

The Synthesized Move Operations

和copy constructor与copy-assignment operator 一样,编译器也提供了默认的constructor和move-assignment operator,但是十分不同。和copy 的操作不同,有一些类根本不会产生move operations的默认函数。特殊的,如果一个类定义了own copy constructor, copy-assignment operator, or destructor,那么就不会产生默认的move operation相关的函数。因此,当一个类没有move operation相关函数的时候,copy operation就会用于替代move operation。

编译器只有当一个类没有任何copy operation相关函数的时候,并且它的每个non-static成员都是可以被move的时候才会给这个类创建默认的move operation函数。即使某个成员是class type,只要这个类可以被move。

如下:

// the compiler will synthesize the move operations for X and hasX
struct X {
    int i; // built-in types can be moved
    std::string s; // string defines its own move operations
};
struct hasX {
    X mem; // X has synthesized move operations
};

和copy operations不同,move operation在默认下永远都不会是delete。然而,如果使用=default来产生默认的move operation,但是编译器不能进行move操作,那么这些move operation就会被定义为deleted。但是有一个例外,其他情况下默认的move operation会被deleted和copy operation有些类似:

  • Unlike the copy constructor, the move constructor is defined as deleted if the class has a member that defines its own copy constructor but does not also define a move constructor, or if the class has a member that doesn’t define its own copy operations and for which the compiler is unable to synthesize a move constructor. Similarly for move-assignment.
  • The move constructor or move-assignment operator is defined as deleted if the class has a member whose own move constructor or move-assignment operator is deleted or inaccessible.
  • Like the copy constructor, the move constructor is defined as deleted if the destructor is deleted or inaccessible.
  • Like the copy-assignment operator, the move-assignment operator is defined as deleted if the class has a const or reference member

还有一些内容不想看了,略,需要的时候再看看。

Classes that define a move constructor or move-assignment operator must also define their own copy operations. Otherwise, those members are deleted by default.

Rvalues Are Moved, Lvalues Are Copied

当一个类都有move constructor和copy constructor的时候,编译器使用普通的函数匹配原则来选择对应的constructor,即最适合的哪个。比如说,StrVec具有一个接收cosnt StrVec&类型的copy constructor,那么它就可以接收任何可以为转为StrVec类型的参数,move constructor 的参数是StrVec&&,只能接收右值。

StrVec v1, v2;
v1 = v2; // v2 is an lvalue; copy assignment
StrVec getVec(istream &); // getVec returns an rvalue
v2 = getVec(cin); // getVec(cin) is an rvalue; move assignment

v1 = v2,前面说过变量是lvalue,因此这里执行都是copy-assignment operator。getVec返回的是一个rvalue,所以v2 = getVec(cin)使用的是move assignment,虽然copy-assignment operator也可以使用,但是这里明显move assignment是最适配的

But Rvalues Are Copied If There Is No Move Constructor

然而,如果一个类只有copy constructor,而没有move constructor怎么办呢?在这种情况下,编译器不会产生默认的move constructor,相反会使用copy constructor 来进行值的赋值,即使我们显式地使用了move

class Foo {
public:
    Foo() = default;
    Foo(const Foo&); // copy constructor
    // other members, but Foo does not define a move constructor
};
Foo x;
Foo y(x); // copy constructor; x is an lvalue
Foo z(std::move(x)); // copy constructor, because there is no move constructor

Foo z(std::move(x))是因为我们可以将Foo&&转为const Foo &,所以创建z使用的是copy constructor。这一点对于=操作符也是一样的,如果没有move assigment,那么就会使用copy assigment。

If a class has a usable copy constructor and no move constructor, objects will be “moved” by the copy constructor. Similarly for the copy-assignment operator and move-assignment.

Copy-and-Swap Assignment Operators and Move

目前来说,似乎如果我们需要省略在复制过程中的开销,那么最好都实现copy assigment和move assigment,以及copy constructr 以及move assgiment 。但是如果我们实现了move constructor,那么也会获得move assignment operator。例子如下:

class HasPtr {
public:
    // added move constructor
    HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) {p.ps = 0;}
    // assignment operator is both the move- and copy-assignment operator
    HasPtr& operator=(HasPtr rhs)
        { swap(*this, rhs); return *this; }
    // other members as in § 13.2.1 (p. 511)
};

HasPtr定义了move constructor,接下来我们看来assignment operator。它的参数类型不是引用类型,所以这里是copy initialized 。取决于参数的类型,copy initialization可以使用move constructor或者是copy constructor。如果是lvalues那么就是调用copy constructor,如果是rvalues,那么调用的就是move constructor。因此,这里的=操作符就具有和copy-assignment以及move assignment operator 一样的功能。

hp = hp2; // hp2 is an lvalue; copy constructor used to copy hp2
hp = std::move(hp2); // move constructor moves hp2

hp = hp2;hp2是一个变量,返回的是左值,所以这里会调用copy constructor来创建临时变量,赋值hp2的内容。std::move(hp2)返回的是右值引用,所以这里会调用move constructor来move hp2的内容,然后返回一个对象。

All five copy-control members should be thought of as a unit: Ordinarily, if a class defines any of these operations, it usually should define them all. As we’ve seen, some classes must define the copy constructor, copy-assignment operator, and destructor to work correctly (§ 13.1.4, p. 504). Such classes typically have a resource that the copy members must copy. Ordinarily, copying a resource entails some amount of overhead. Classes that define the move constructor and move-assignment operator can avoid this overhead in those circumstances where a copy isn’t necessary.

Move Iterators

之前介绍的很多函数都是使用iterator来进行复制数据,可以使用move iterator来进行move数据。move iterator 修改了所给的iterator的dereference的行为。

A move iterator adapts its given iterator by changing the behavior of the iterator’s dereference operator.

通常的iterator返回的是一个左值引用,move iterator返回的是一个右值引用。我们可以通过make_move_iterator将一个普通的iterator转为move iterator。如下。标准库并没有保证哪些函数可以和move iterator进行搭配,因为move会影响原对象的数据,所以最好十分肯定我们不会再去使用原对象的数据,才可以在它上面使用move itetrator,下面是一个move iterator的用法例子:

std::list<std::string> s{"one", "two", "three"};
std::vector<std::string> v1(s.begin(), s.end()); // copy
std::vector<std::string> v2(std::make_move_iterator(s.begin()),
                            std::make_move_iterator(s.end())); // move
print("v1 now holds: ", v1);
print("v2 now holds: ", v2);
print("original list now holds: ", s);

Rvalue References and Member Functions

copy and move versions可以对成员函数带来好处。一个成员通常接收两种不同的函数签名,一个接收const lvalue reference,另外一个是rvalue reference。如下是一个push_back的例子:

void push_back(const X&); // copy: binds to any kind of X
void push_back(X&&); // move: binds only to modifiable rvalues of type X

push_back(const X&)可以接收任何数据类型,只要改类型可以转为X。这个版本的push_back会进行复制,因为const引用会创建一个临时变量。而第二个版本接收的是右值引用。通常并不需要创建const X&&X&类型的函数。首先因为,对于右值引用来说,我们想从它哪里偷得数据,因此argument不能是const。

Overloaded functions that distinguish between moving and copying a parameter typically have one version that takes a const T& and one that takes a T&&

看如下例子:

StrVec vec; // empty StrVec
string s = "some string or another";
vec.push_back(s); // calls push_back(const string&)
vec.push_back("done"); // calls push_back(string&&)

vec.push_back("done")会调用string的string(const char *) 进行类型转换,然后因为函数的返回的是右值,所以就和push_back(string&&)进行了匹配。那么这里是否会调用move constructor 呢?答案是不会的。接下来介绍以下move constructor被调用的情况,以下内容来自cppreference:

  • initialization: T a = std::move(b); or T a(std::move(b));, where b is of type T;
  • function argument passing: f(std::move(a));, where a is of type T and f is void f(T t);
  • function return: return a; inside a function such as T f(), where a is of type T which has a move constructor.

When the initializer is a prvalue, the move constructor call is often optimized out (until C++17)never made (since C++17), see copy elision.

测试代码如下:

class Foo {
public:
    Foo() {
        cout << "default" << endl;
    }
    Foo(Foo &f) {
        cout << "copy" << endl;
    }

    Foo(Foo &&f) noexcept{
        cout << "move" << endl;
    }
};
void test(Foo &&f1) {

}
Foo test1() {
    cout<< "here" << endl;
    return Foo(); // 需要-fno-elide-constructors选项
}
int main()
{
    Foo a;
    Foo a1;
    Foo a2;
    cout << "-------------" << endl;
    Foo b = std::move(a);   // 隐式类型转换
    Foo c = Foo(std::move(a1));
    test(std::move(a2));
    test1();
    return 0;
}

主要的就是在作为返回值的时候,prvalue会被优化,进而不会调用move constructor,加上-fno-elide-constructors来关掉这个优化,不然看在返回值中不会调用move constructor。

Rvalue and Lvalue Reference Member Functions

通常,我们可以在对象上调用成员函数,无论这个对象是lvalue还是rvalue,如下:

string s1 = "a value", s2 = "another";
auto n = (s1 + s2).find('a');

甚至,还有,更有让人惊讶的代码:

s1 + s2 = "wow!";

在先前的标准中,没有方法来杜绝这种写法,为了保证兼容性,这个代码在现在也是允许的。**然而,我们可以在自己的类当中杜绝这种写法,我们可以强制左操作数是lvalue。语法如下:

Foo &operator=(const Foo&) &;

而且reference qualifier可以和const搭配在一起,但是它必须出现在const之后,如下:

Foo anotherMem() const &; 

Overloading and Reference Functions

前面说过我们可以根据const来进行重载,const对象只可以调用const的成员方法。相类似的,我们也可以根据reference qualifier以及const来重载成员方法。

class Foo {
public:
    Foo sorted() &&; // may run on modifiable rvalues
    Foo sorted() const &; // may run on any kind of Foo
    // other members of Foo
private:
    vector<int> data;
};
// this object is an rvalue, so we can sort in place
Foo Foo::sorted() &&
{
    sort(data.begin(), data.end());
    return *this;
}
    // this object is either const or it is an lvalue; either way we can't sort in place
    Foo Foo::sorted() const & {
    Foo ret(*this); // make a copy
    sort(ret.data.begin(), ret.data.end()); // sort the copy
    return ret; // return the copy
}

当我们在rvalue上调用stored时候,直接在上面排序也是没有任何影响的,因为没有其他人在使用rvalue。当我们在const rvalue或者是lvalue上操作的时候,我们不能修改原来的对象,否则会对原来的对象造成影响,所以进行了复制。

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇