Take the Dynarray as an example.
class Dynarray {
int *m_storage;
std::size_t m_length;
public:
Dynarray(const Dynarray &other) // copy constructor
: m_storage(new int[other.m_length]), m_length(other.m_length) {
for (std::size_t i = 0; i != m_length; ++i)
m_storage[i] = other.m_storage[i];
}
Dynarray(Dynarray &&other) noexcept // move constructor
: m_storage(other.m_storage), m_length(other.m_length) {
other.m_storage = nullptr;
other.m_length = 0;
}
};
class Dynarray {
int *m_storage;
std::size_t m_length;
public:
Dynarray(Dynarray &&other) noexcept // move constructor
: m_storage(other.m_storage), m_length(other.m_length) {
}
};
other, instead of making a copy.class Dynarray {
int *m_storage;
std::size_t m_length;
public:
Dynarray(Dynarray &&other) noexcept // move constructor
: m_storage(other.m_storage), m_length(other.m_length) {
other.m_storage = nullptr;
other.m_length = 0;
}
};
other, instead of making a copy.other is in a valid state, so that it can be safely destroyed.* Take ownership of other's resources!
Take ownership of other's resources!
class Dynarray {
public:
Dynarray &operator=(Dynarray &&other) noexcept {
m_storage = other.m_storage; m_length = other.m_length;
return *this;
}
};
other.class Dynarray {
public:
Dynarray &operator=(Dynarray &&other) noexcept {
m_storage = other.m_storage; m_length = other.m_length;
other.m_storage = nullptr; other.m_length = 0;
return *this;
}
};
other.other is in a valid state, so that it can be safely destroyed.Are we done?
class Dynarray {
public:
Dynarray &operator=(Dynarray &&other) noexcept {
delete[] m_storage;
m_storage = other.m_storage; m_length = other.m_length;
other.m_storage = nullptr; other.m_length = 0;
return *this;
}
};
other.other is in a valid state, so that it can be safely destroyed.Are we done?
class Dynarray {
public:
Dynarray &operator=(Dynarray &&other) noexcept {
if (this != &other) {
delete[] m_storage;
m_storage = other.m_storage; m_length = other.m_length;
other.m_storage = nullptr; other.m_length = 0;
}
return *this;
}
};
other.other is in a valid state, so that it can be safely destroyed.* Self-assignment safe!
Before we move on, let's define a function for demonstration.
Suppose we have a function that concatenates two Dynarrays:
Dynarray concat(const Dynarray &a, const Dynarray &b) {
Dynarray result(a.size() + b.size());
for (std::size_t i = 0; i != a.size(); ++i)
result.at(i) = a.at(i);
for (std::size_t i = 0; i != b.size(); ++i)
result.at(a.size() + i) = b.at(i);
return result;
}
Which assignment operator should be called?
a = concat(b, c);
Lvalues are copied; rvalues are moved ...
a = concat(b, c); // calls move assignment operator,
// because `concat(b, c)` is an rvalue.
a = b; // calls copy assignment operator
Lvalues are copied; rvalues are moved ...
a = concat(b, c); // calls move assignment operator,
// because `concat(b, c)` generates an rvalue.
a = b; // copy assignment operator
... but rvalues are copied if there is no move operation.
// If Dynarray has no move assignment operator, this is a copy assignment.
a = concat(b, c)
Like copy operations, we can use =default to require a synthesized move operation that has the default behaviors.
struct X {
X(X &&) = default;
X &operator=(X &&) = default;
};
X(X&& o) : mem1(std::move(o.mem1)), mem2(std::move(o.mem2)), ... {}. (Later we will talk about std::move.)noexcept if all the operations involved are noexcept.Move operations can also be deleted by =delete, but be careful ...
The updated copy control members:
If one of them has a user-provided version, the copy control of the class is thought of to have special behaviors. (Recall "the rule of three".)
deleted if the class has a user-provided move operation.This is why some of you see this error:
Implicitly-declared copy assignment operator is deprecated, because the
class has a user-provided copy constructor.
The copy control members in modern C++:
The Rule of Five: Define zero or five of them.
Suppose we give our Dynarray a label:
class Dynarray {
int *m_storage;
std::size_t m_length;
std::string m_label;
};
The move assignment operator should invoke the move assignment operator on m_label. But how?
m_label = other.m_label; // calls copy assignment operator,
// because `other.m_label` is an lvalue.
std::movestd::moveDefined in <utility>
std::move(x) performs an lvalue to rvalue cast:
int ival = 42;
int &&rref = ival; // Error
int &&rref2 = std::move(ival); // Correct
Calling std::move(x) tells the compiler that:
x is an lvalue, butx as an rvalue.std::movestd::move(x) indicates that we want to treat x as an rvalue, which means that x will be moved from.
The call to std::move promises that we do not intend to use x again,
A call to std::move is usually followed by a call to some function that moves the object, after which we cannot make any assumptions about the value of the moved-from object.
void foo(X &&x); // moves `x`
void foo(const X &x); // copies `x`
foo(std::move(x)); // matches `foo(X&&)`, so that `x` is moved.
"std::move does not move anything. It just makes a promise."
std::moveSuppose we give every Dynarray a special "label", which is a string.
class Dynarray {
int *m_storage;
std::size_t m_length;
std::string m_label;
public:
Dynarray(Dynarray &&other) noexcept
: m_storage(other.m_storage), m_length(other.m_length),
m_label(std::move(other.m_label)) { // !!
other.m_storage = nullptr;
other.m_length = 0;
}
};
The standard library facilities ought to define efficient and correct move operations.
std::moveSuppose we give every Dynarray a special "label", which is a string.
class Dynarray {
int *m_storage;
std::size_t m_length;
std::string m_label;
public:
Dynarray &operator=(Dynarray &&other) noexcept {
if (this != &other) {
delete[] m_storage;
m_storage = other.m_storage; m_length = other.m_length;
m_label = std::move(other.m_label);
other.m_storage = nullptr; other.m_length = 0;
}
return *this;
}
};
The standard library facilities ought to define efficient and correct move operations.
std::moveWhy do we need std::move?
class Dynarray {
public:
Dynarray(Dynarray &&other) noexcept
: m_storage(other.m_storage), m_length(other.m_length),
m_label(other.m_label) { // Isn't this correct?
other.m_storage = nullptr;
other.m_length = 0;
}
};
other is an rvalue reference, so ... ?
other is an rvalue reference, which is an lvalue.
std::move.class Dynarray {
public:
Dynarray(Dynarray &&other) noexcept
: m_storage(other.m_storage), m_length(other.m_length),
m_label(other.m_label) { // `other.m_label` is copied, not moved.
other.m_storage = nullptr;
other.m_length = 0;
}
};
An rvalue reference is an lvalue! Does that make sense?
The lifetime of rvalues is often very short, compared to that of lvalues.
An rvalue reference extends the lifetime of the rvalue that it is bound to.
std::string s1 = something(), s2 = some_other_thing();
std::string &&rr = s1 + s2; // The state of the temporary object is "captured"
// by the rvalue reference, without which the
// temporary object will be destroyed.
std::cout << rr << '\n'; // Now we can use `rr` just like a normal string.
Golden rule: Anything that has a name is an lvalue.
std::string foo(const std::string &a, const std::string &b) {
return a + b; // a temporary
}
std::string s = foo(a, b);
a + b.std::string foo(const std::string &a, const std::string &b) {
return a + b; // a temporary
}
std::string s = foo(a, b);
Since C++17, no copy or move is made here. The initialization of s is the same as
std::string s(a + b);
This is called copy elision.
Dynarray concat(const Dynarray &a, const Dynarray &b) {
Dynarray result(a.size() + b.size());
for (std::size_t i = 0; i != a.size(); ++i)
result.at(i) = a.at(i);
for (std::size_t i = 0; i != b.size(); ++i)
result.at(a.size() + i) = b.at(i);
return result;
}
a = concat(b, c);
result is a local object of concat.return result performs a move initialization of a temporary object, say tmp.a is performed.Dynarray concat(const Dynarray &a, const Dynarray &b) {
Dynarray result(a.size() + b.size());
// ...
return result;
}
Dynarray a = concat(b, c); // Initialization
NRVO transforms this code to
// Pseudo C++ code.
void concat(Dynarray &result, const Dynarray &a, const Dynarray &b) {
// Pseudo C++ code. For demonstration only.
result.Dynarray::Dynarray(a.size() + b.size()); // construct in-place
// ...
}
Dynarray a@; // Uninitialized.
concat(a@, b, c);
so that no copy or move is needed.
Note:
Rvalue references
X && and const X & can be overloaded.Move operations
=defaultstd::move
std::move(x) makes a promise that x can be safely moved from.In modern C++, unnecessary copies are greatly avoided by:
returned lvalue treated as an rvalue, andstd::move applied to an lvalue.
An implicitly deleted move operation will be ignored in overload resolution. This is addressed by the defect report CWG 1402 and is applied retroactively to C++11. Note that this change of behavior did not come into effect when the book C++ Primer, 5e was published.