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.
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.
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.
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.
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 reallocate
(§ 13.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
.
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.
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.
After a move operation, the “moved-from” object must remain a valid, destructible object but users may make no assumptions about its value.
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
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 = default
(§ 7.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.
Classes that define a move constructor or move-assignment operator must also define their own copy operations. Otherwise, those members are deleted by default.
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.
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.
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.
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 noexcept
(§ 13.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 swap
s 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.
Message
ClassClasses 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 Folder
s, 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 noexcept
(§ 13.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 Folder
s, we call move
to move the contents
from rhs
to this
object. What remains is to call move_Messages
to update the Folder
pointers.
The reallocate
member of StrVec
(§ 13.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.
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.
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_ptr
s 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.