Files
NoteNextra-origin/content/CSE332S/CSE332S_L15.md
2025-09-17 14:27:46 -05:00

149 lines
4.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# CSE332S Object-Oriented Programming in C++ (Lecture 15)
## Move semantics introduction and motivation
Review: copy control consists of 5 distinct operations
- A `copy constructor` initializes an object by duplicating the const l-value that was passed to it by reference
- A `copy-assignment operator` (re)sets an objects value by duplicating the const l-value passed to it by reference
- A `destructor` manages the destruction of an object
- A `move constructor` initializes an object by transferring the implementation from the r-value reference passed to it
- A `move-assignment operator` (re)sets an objects 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::move` etc.
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 wont be used for anything afterward
- Also, must be safe to destroy i (could be stack/heap/global)
### Move Constructors
```cpp
// 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 `a` is 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 `a` to current object
```cpp
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 as size value
values_ = a.values_; // take as pointer value
a.size_ = 0; // zero out as size
a.values_ = nullptr; // zero out as 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=` from `Derived::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