13.6.2. Move Constructor and Move Assignment

Image

Like the string class (and other library classes), our own classes can benefit from being able to be moved as well as copied. To enable move operations for our own types, we define a move constructor and a move-assignment operator. These members are similar to the corresponding copy operations, but they “steal” resources from their given object rather than copy them.

Image

Like the copy constructor, the move constructor has an initial parameter that is a reference to the class type. Differently from the copy constructor, the reference parameter in the move constructor is an rvalue reference. As in the copy constructor, any additional parameters must all have default arguments.

In addition to moving resources, the move constructor must ensure that the moved-from object is left in a state such that destroying that object will be harmless. In particular, once its resources are moved, the original object must no longer point to those moved resources—responsibility for those resources has been assumed by the newly created object.

As an example, we’ll define the StrVec move constructor to move rather than copy the elements from one StrVec to another:

StrVec::StrVec(StrVec &&s) noexcept   // move won't throw any exceptions
  // 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;
}

We’ll explain the use of noexcept (which signals that our constructor does not throw any exceptions) shortly, but let’s first look at what this constructor does.

Unlike the copy constructor, the move constructor does not allocate any new memory; it takes over the memory in the given StrVec. Having taken over the memory from its argument, the constructor body sets the pointers in the given object to nullptr. After an object is moved from, that object continues to exist. Eventually, the moved-from object will be destroyed, meaning that the destructor will be run on that object. The StrVec destructor calls deallocate on first_free. If we neglected to change s.first_free, then destroying the moved-from object would delete the memory we just moved.

Move Operations, Library Containers, and Exceptions
Image

Because a move operation executes by “stealing” resources, it ordinarily does not itself allocate any resources. As a result, move operations ordinarily will not throw any exceptions. When we write a move operation that cannot throw, we should inform the library of that fact. As we’ll see, unless the library knows that our move constructor won’t throw, it will do extra work to cater to the possibliity that moving an object of our class type might throw.

Image

One way inform the library is to specify noexcept on our constructor. We’ll cover noexcept, which was introduced by the new standard, in more detail in § 18.1.4 (p. 779). For now what’s important to know is that noexcept is a way for us to promise that a function does not throw any exceptions. We specify noexcept on a function after its parameter list. In a constructor, noexcept appears between the parameter list and the : that begins the constructor initializer list:

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

We must specify noexcept on both the declaration in the class header and on the definition if that definition appears outside the class.


Image Note

Move constructors and move assignment operators that cannot throw exceptions should be marked as noexcept.


Understanding why noexcept is needed can help deepen our understanding of how the library interacts with objects of the types we write. We need to indicate that a move operation doesn’t throw because of two interrelated facts: First, although move operations usually don’t throw exceptions, they are permitted to do so. Second, the library containers provide guarantees as to what they do if an exception happens. As one example, vector guarantees that if an exception happens when we call push_back, the vector itself will be left unchanged.

Now let’s think about what happens inside push_back. Like the corresponding StrVec operation (§ 13.5, p. 527), push_back on a vector might require that the vector be reallocated. When a vector is reallocated, it moves the elements from its old space to new memory, just as we did in reallocate13.5, p. 530).

As we’ve just seen, moving an object generally changes the value of the moved-from object. If reallocation uses a move constructor and that constructor throws an exception after moving some but not all of the elements, there would be a problem. The moved-from elements in the old space would have been changed, and the unconstructed elements in the new space would not yet exist. In this case, vector would be unable to meet its requirement that the vector is left unchanged.

On the other hand, if vector uses the copy constructor and an exception happens, it can easily meet this requirement. In this case, while the elements are being constructed in the new memory, the old elements remain unchanged. If an exception happens, vector can free the space it allocated (but could not successfully construct) and return. The original vector elements still exist.

To avoid this potential problem, vector must use a copy constructor instead of a move constructor during reallocation unless it knows that the element type’s move constructor cannot throw an exception. If we want objects of our type to be moved rather than copied in circumstances such as vector reallocation, we must explicity tell the library that our move constructor is safe to use. We do so by marking the move constructor (and move-assignment operator) noexcept.

Move-Assignment Operator

The move-assignment operator does the same work as the destructor and the move constructor. As with the move constructor, if our move-assignment operator won’t throw any exceptions, we should make it noexcept. Like a copy-assignment operator, a move-assignment operator must guard against self-assignment:

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

In this case we check directly whether the this pointer and the address of rhs are the same. If they are, the right- and left-hand operands refer to the same object and there is no work to do. Otherwise, we free the memory that the left-hand operand had used, and then take over the memory from the given object. As in the move constructor, we set the pointers in rhs to nullptr.

It may seem surprising that we bother to check for self-assignment. After all, move assignment requires an rvalue for the right-hand operand. We do the check because that rvalue could be the result of calling move. As in any other assignment operator, it is crucial that we not free the left-hand resources before using those (possibly same) resources from the right-hand operand.

A Moved-from Object Must Be Destructible
Image

Moving from an object does not destroy that object: Sometime after the move operation completes, the moved-from object will be destroyed. Therefore, when we write a move operation, we must ensure that the moved-from object is in a state in which the destructor can be run. Our StrVec move operations meet this requirement by setting the pointer members of the moved-from object to nullptr.

In addition to leaving the moved-from object in a state that is safe to destroy, move operations must guarantee that the object remains valid. In general, a valid object is one that can safely be given a new value or used in other ways that do not depend on its current value. On the other hand, move operations have no requirements as to the value that remains in the moved-from object. As a result, our programs should never depend on the value of a moved-from object.

For example, when we move from a library string or container object, we know that the moved-from object remains valid. As a result, we can run operations such as as empty or size on moved-from objects. However, we don’t know what result we’ll get. We might expect a moved-from object to be empty, but that is not guaranteed.

Our StrVec move operations leave the moved-from object in the same state as a default-initialized object. Therefore, all the operations of StrVec will continue to run the same way as they do for any other default-initialized StrVec. Other classes, with more complicated internal structure, may behave differently.


Image Warning

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

As it does for the copy constructor and copy-assignment operator, the compiler will synthesize the move constructor and move-assignment operator. However, the conditions under which it synthesizes a move operation are quite different from those in which it synthesizes a copy operation.

Recall that if we do not declare our own copy constructor or copy-assignment operator the compiler always synthesizes these operations (§ 13.1.1, p. 497 and § 13.1.2, p. 500). The copy operations are defined either to memberwise copy or assign the object or they are defined as deleted functions.

Differently from the copy operations, for some classes the compiler does not synthesize the move operations at all. In particular, if a class defines its own copy constructor, copy-assignment operator, or destructor, the move constructor and move-assignment operator are not synthesized. As a result, some classes do not have a move constructor or a move-assignment operator. As we’ll see on page 540, when a class doesn’t have a move operation, the corresponding copy operation is used in place of move through normal function matching.

The compiler will synthesize a move constructor or a move-assignment operator only if the class doesn’t define any of its own copy-control members and if every nonstatic data member of the class can be moved. The compiler can move members of built-in type. It can also move members of a class type if the member’s class has the corresponding move operation:

// 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
};
X x, x2 = std::move(x);       // uses the synthesized move constructor
hasX hx, hx2 = std::move(hx); // uses the synthesized move constructor


Image Note

The compiler synthesizes the move constructor and move assignment only if a class does not define any of its own copy-control members and only if all the data members can be moved constructed and move assigned, respectively.


Unlike the copy operations, a move operation is never implicitly defined as a deleted function. However, if we explicitly ask the compiler to generate a move operation by using = default7.1.4, p. 264), and the compiler is unable to move all the members, then the move operation will be defined as deleted. With one important exception, the rules for when a synthesized move operation is defined as deleted are analogous to those for the copy operations (§ 13.1.6, p. 508):

• 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.

For example, assuming Y is a class that defines its own copy constructor but does not also define its own move constructor:

// assume Y is a class that defines its own copy constructor but not a move constructor
struct hasY {
    hasY() = default;
    hasY(hasY&&) = default;
    Y mem; // hasY will have a deleted move constructor
};
hasY hy, hy2 = std::move(hy); // error: move constructor is deleted

The compiler can copy objects of type Y but cannot move them. Class hasY explicitly requested a move constructor, which the compiler is unable to generate. Hence, hasY will get a deleted move constructor. Had hasY omitted the declaration of its move constructor, then the compiler would not synthesize the hasY move constructor at all. The move operations are not synthesized if they would otherwise be defined as deleted.

There is one final interaction between move operations and the synthesized copy-control members: Whether a class defines its own move operations has an impact on how the copy operations are synthesized. If the class defines either a move constructor and/or a move-assignment operator, then the synthesized copy constructor and copy-assignment operator for that class will be defined as deleted.


Image Note

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 ...

When a class has both a move constructor and a copy constructor, the compiler uses ordinary function matching to determine which constructor to use (§ 6.4, p. 233). Similarly for assignment. For example, in our StrVec class the copy versions take a reference to const StrVec. As a result, they can be used on any type that can be converted to StrVec. The move versions take a StrVec&& and can be used only when the argument is a (nonconst) rvalue:

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

In the first assignment, we pass v2 to the assignment operator. The type of v2 is StrVec and the expression, v2, is an lvalue. The move version of assignment is not viable (§ 6.6, p. 243), because we cannot implicitly bind an rvalue reference to an lvalue. Hence, this assignment uses the copy-assignment operator.

In the second assignment, we assign from the result of a call to getVec. That expression is an rvalue. In this case, both assignment operators are viable—we can bind the result of getVec to either operator’s parameter. Calling the copy-assignment operator requires a conversion to const, whereas StrVec&& is an exact match. Hence, the second assignment uses the move-assignment operator.

...But Rvalues Are Copied If There Is No Move Constructor

What if a class has a copy constructor but does not define a move constructor? In this case, the compiler will not synthesize the move constructor, which means the class has a copy constructor but no move constructor. If a class has no move constructor, function matching ensures that objects of that type are copied, even if we attempt to move them by calling 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

The call to move(x) in the initialization of z returns a Foo&& bound to x. The copy constructor for Foo is viable because we can convert a Foo&& to a const Foo&. Thus, the initialization of z uses the copy constructor for Foo.

It is worth noting that using the copy constructor in place of a move constructor is almost surely safe (and similarly for the assignment operators). Ordinarily, the copy constructor will meet the requirements of the corresponding move constructor: It will copy the given object and leave that original object in a valid state. Indeed, the copy constructor won’t even change the value of the original object.


Image Note

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

The version of our HasPtr class that defined a copy-and-swap assignment operator (§ 13.3, p. 518) is a good illustration of the interaction between function matching and move operations. If we add a move constructor to this class, it will effectively get a move assignment operator as well:

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)
};

In this version of the class, we’ve added a move constructor that takes over the values from its given argument. The constructor body sets the pointer member of the given HasPtr to zero to ensure that it is safe to destroy the moved-from object. Nothing this function does can throw an exception so we mark it as noexcept13.6.2, p. 535).

Now let’s look at the assignment operator. That operator has a nonreference parameter, which means the parameter is copy initialized (§ 13.1.1, p. 497). Depending on the type of the argument, copy initialization uses either the copy constructor or the move constructor; lvalues are copied and rvalues are moved. As a result, this single assignment operator acts as both the copy-assignment and move-assignment operator.

For example, assuming both hp and hp2 are HasPtr objects:

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

In the first assignment, the right-hand operand is an lvalue, so the move constructor is not viable. The copy constructor will be used to initialize rhs. The copy constructor will allocate a new string and copy the string to which hp2 points.

In the second assignment, we invoke std::move to bind an rvalue reference to hp2. In this case, both the copy constructor and the move constructor are viable. However, because the argument is an rvalue reference, it is an exact match for the move constructor. The move constructor copies the pointer from hp2. It does not allocate any memory.

Regardless of whether the copy or move constructor was used, the body of the assignment operator swaps the state of the two operands. Swapping a HasPtr exchanges the pointer (and int) members of the two objects. After the swap, rhs will hold a pointer to the string that had been owned by the left-hand side. That string will be destroyed when rhs goes out of scope.

Move Operations for the Message Class

Classes that define their own copy constructor and copy-assignment operator generally also benefit by defining the move operations. For example, our Message and Folder classes (§ 13.4, p. 519) should define move operations. By defining move operations, the Message class can use the string and set move operations to avoid the overhead of copying the contents and folders members.

However, in addition to moving the folders member, we must also update each Folder that points to the original Message. We must remove pointers to the old Message and add a pointer to the new one.

Both the move constructor and move-assignment operator need to update the Folder pointers, so we’ll start by defining an operation to do this common work:

// move the Folder pointers from m to this Message
void Message::move_Folders(Message *m)
{
    folders = std::move(m->folders); // uses set move assignment
    for (auto f : folders) {  // for each Folder
        f->remMsg(m);    // remove the old Message from the Folder
        f->addMsg(this); // add this Message to that Folder
    }
    m->folders.clear();  // ensure that destroying m is harmless
}

This function begins by moving the folders set. By calling move, we use the set move assignment rather than its copy assignment. Had we omitted the call to move, the code would still work, but the copy is unnecessary. The function then iterates through those Folders, removing the pointer to the original Message and adding a pointer to the new Message.

It is worth noting that inserting an element to a set might throw an exception—adding an element to a container requires memory to be allocated, which means that a bad_alloc exception might be thrown (§ 12.1.2, p. 460). As a result, unlike our HasPtr and StrVec move operations, the Message move constructor and move-assignment operators might throw exceptions. We will not mark them as noexcept13.6.2, p. 535).

The function ends by calling clear on m.folders. After the move, we know that m.folders is valid but have no idea what its contents are. Because the Message destructor iterates through folders, we want to be certain that the set is empty.

The Message move constructor calls move to move the contents and default initializes its folders member:

Message::Message(Message &&m): contents(std::move(m.contents))
{
    move_Folders(&m); // moves folders and updates the Folder pointers
}

In the body of the constructor, we call move_Folders to remove the pointers to m and insert pointers to this Message.

The move-assignment operator does a direct check for self-assignment:

Message& Message::operator=(Message &&rhs)
{
    if (this != &rhs) {       // direct check for self-assignment
        remove_from_Folders();
        contents = std::move(rhs.contents); // move assignment
        move_Folders(&rhs); // reset the Folders to point to this Message
    }
    return *this;
}

As with any assignment operator, the move-assignment operator must destroy the old state of the left-hand operand. In this case, destroying the left-hand operand requires that we remove pointers to this Message from the existing folders, which we do in the call to remove_from_Folders. Having removed itself from its Folders, we call move to move the contents from rhs to this object. What remains is to call move_Messages to update the Folder pointers.

Move Iterators

The reallocate member of StrVec13.5, p. 530) used a for loop to call construct to copy the elements from the old memory to the new. As an alternative to writing that loop, it would be easier if we could call uninitialized_copy to construct the newly allocated space. However, uninitialized_copy does what it says: It copies the elements. There is no analogous library function to “move” objects into unconstructed memory.

Instead, the new library defines a move iterator adaptor (§ 10.4, p. 401). A move iterator adapts its given iterator by changing the behavior of the iterator’s dereference operator. Ordinarily, an iterator dereference operator returns an lvalue reference to the element. Unlike other iterators, the dereference operator of a move iterator yields an rvalue reference.

Image

We transform an ordinary iterator to a move iterator by calling the library make_move_iterator function. This function takes an iterator and returns a move iterator.

All of the original iterator’s other operations work as usual. Because these iterators support normal iterator operations, we can pass a pair of move iterators to an algorithm. In particular, we can pass move iterators to uninitialized_copy:

void StrVec::reallocate()
{
    // allocate space for twice as many elements as the current size
    auto newcapacity = size() ? 2 * size() : 1;
    auto first = alloc.allocate(newcapacity);
    // move the elements
    auto last = uninitialized_copy(make_move_iterator(begin()),
                                   make_move_iterator(end()),
                                   first);
    free();             // free the old space
    elements = first;   // update the pointers
    first_free = last;
    cap = elements + newcapacity;
}

uninitialized_copy calls construct on each element in the input sequence to “copy” that element into the destination. That algorithm uses the iterator dereference operator to fetch elements from the input sequence. Because we passed move iterators, the dereference operator yields an rvalue reference, which means construct will use the move constructor to construct the elements.

It is worth noting that standard library makes no guarantees about which algorithms can be used with move iterators and which cannot. Because moving an object can obliterate the source, you should pass move iterators to algorithms only when you are confident that the algorithm does not access an element after it has assigned to that element or passed that element to a user-defined function.


Exercises Section 13.6.2

Exercise 13.49: Add a move constructor and move-assignment operator to your StrVec, String, and Message classes.

Exercise 13.50: Put print statements in the move operations in your String class and rerun the program from exercise 13.48 in § 13.6.1 (p. 534) that used a vector<String> to see when the copies are avoided.

Exercise 13.51: Although unique_ptrs cannot be copied, in § 12.1.5 (p. 471) we wrote a clone function that returned a unique_ptr by value. Explain why that function is legal and how it works.

Exercise 13.52: Explain in detail what happens in the assignments of the HasPtr objects on page 541. In particular, describe step by step what happens to values of hp, hp2, and of the rhs parameter in the HasPtr assignment operator.

Exercise 13.53: As a matter of low-level efficiency, the HasPtr assignment operator is not ideal. Explain why. Implement a copy-assignment and move-assignment operator for HasPtr and compare the operations executed in your new move-assignment operator versus the copy-and-swap version.

Exercise 13.54: What would happen if we defined a HasPtr move-assignment operator but did not change the copy-and-swap operator? Write code to test your answer.


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.176.194