CSE332S Object-Oriented Programming in C++ (Lecture 15)
Move semantics introduction and motivation
Review: copy control consists of 5 distinct operations
- A
copy constructorinitializes an object by duplicating the const l-value that was passed to it by reference - A
copy-assignment operator(re)sets an object’s value by duplicating the const l-value passed to it by reference - A
destructormanages the destruction of an object - A
move constructorinitializes an object by transferring the implementation from the r-value reference passed to it - A
move-assignment operator(re)sets an object’s value by transferring the implementation from the r-value reference passed to it
Today we’ll focus on the last 2 operations and other features (introduced in C++11) like r-value references
I.e., features that support the new C++11 move semantics
Motivation for move semantics
Copy construction and copy-assignment may be expensive due to time/memory for copying It would be more efficient to simply “take” the implementation from the passed object, if that’s ok It’s ok if the passed object won’t be used afterward
- E.g., if it was passed by value and so is a temporary object
- E.g., if a special r-value reference says it’s ok to take from (as long as object remains in a state that’s safe to destruct)
Note that some objects require move semantics
- I.e., types that don’t allow copy construction/assignment
- E.g.,
unique_ptr,ifstream,thread, etc.
New for C++11: r-value references and move function
- E.g.,
int i; int &&rvri = std::move(i);
Synthesized move operations
Compiler will only synthesize a move operation if
- Class does not declare any copy control operations, and
- Every non-static data member of the class can be moved
Members of built-in types can be moved
- E.g., by
std::moveetc.
User-defined types that have synthesized/defined version of the specific move operation can be moved L-values are always copied, r-values can be moved
- If there is no move constructor, r-values only can be copied
Can ask for a move operation to be synthesized
- I.e., by using
= default - But if cannot move all members, synthesized as
= delete
Move constructor and assignment operator examples, more details on inheritance
R-values, L-values, and Reference to Either
A variable is an l-value (has a location)
- E.g.,
int i = 7;
Can take a regular (l-value) reference to it
- E.g.,
int & lvri = i;
An expression is an r-value
- E.g.,
i * 42
Can only take an r-value reference to it (note syntax)
- E.g.,
int && rvriexp = i * 42;
Can only get r-value reference to l-value via move
- E.g.,
int && rvri = std::move(i); - Promises that i won’t be used for anything afterward
- Also, must be safe to destroy i (could be stack/heap/global)
Move Constructors
// takes implementation from a
IntArray::IntArray(IntArray &&a)
: size_(a.size_),
values_(a.values_) {
// make a safe to destroy
a.values_ = nullptr;
a.size_ = 0;
}Note r-value reference
- Says it’s safe to take a’s implementation from it
- Promises only subsequent operation will be destruction
Note constructor design
- A lot like shallow copy constructor’s implementation
- Except, zeroes out state of
a - No sharing, current object owns the implementation
- Object
ais now safe to destroy (but is not safe to do anything else with afterward)
Move Assignment Operator
No allocation, so no exceptions to worry about
- Simply free existing implementation (delete
values_) - Then copy over size and pointer values from
a - Then zero out size and pointer in
a
This leaves assignment complete, a safe to destroy
- Implementation is transferred from
ato current object
Array & Array::operator=(Array &&a) { // Note r-value reference
if (&a != this) { // still test for self-assignment
delete [] values_; // safe to free first (if not self-assigning)
size_ = a. size_; // take a’s size value
values_ = a.values_; // take a’s pointer value
a.size_ = 0; // zero out a’s size
a.values_ = nullptr; // zero out a’s pointer (now safe to destroy)
}
return *this;
}Move Semantics and Inheritance
Base classes should declare/define move operations
- If it makes sense to do so at all
- Derived classes then can focus on moving their members
- E.g., calling
Base::operator=fromDerived::operator=
Containers further complicate these issues
- Containers hold their elements by value
- Risks slicing, other inheritance and copy control problems
So, put (smart) pointers, not objects, into containers
- Access is polymorphic if destructors, other methods virtual
- Smart pointers may help reduce need for copy control operations, or at least simplify cases where needed