225 lines
7.1 KiB
Markdown
225 lines
7.1 KiB
Markdown
# CSE332S Object-Oriented Programming in C++ (Lecture 14)
|
|
|
|
## Copy control
|
|
|
|
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 object's 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 (next lecture)
|
|
- A `move-assignment operator` (re)sets an object's value by transferring the implementation from the r-value reference passed to it (next lecture)
|
|
|
|
Today we'll focus on the first 3 operations and will defer the others (introduced in C++11) until next time
|
|
|
|
- The others depend on the new C++11 `move semantics`
|
|
|
|
### Basic copy control operations
|
|
|
|
A copy constructor or copy-assignment operator takes a reference to a (usually const) instance of the class
|
|
|
|
- Copy constructor initializes a new object from it
|
|
- Copy-assignment operator sets object's value from it
|
|
- In either case, original the object is left unchanged (which differs from the move versions of these operations)
|
|
- Destructor takes no arguments `~A()` (except implicit `this`)
|
|
|
|
Copy control operations for built-in types
|
|
|
|
- Copy construction and copy-assignment copy values
|
|
- Destructor of built-in types does nothing (is a "no-op")
|
|
|
|
Compiler-synthesized copy control operations
|
|
|
|
- Just call that same operation on each member of the object
|
|
- Uses defined/synthesized definition of that operation for user-defined types (see above for built-in types)
|
|
|
|
### Preventing or Allowing Basic Copy Control
|
|
|
|
Old (C++03) way to prevent compiler from generating a default constructor, copy constructor, destructor, or assignment operator was somewhat awkward
|
|
|
|
- Declare private, don't define, don't use within class
|
|
- This works, but gives cryptic linker error if operation is used
|
|
|
|
New (C++11) way to prevent calls to any method
|
|
|
|
- End the declaration with `= delete` (and don't define)
|
|
- Compiler will then give an intelligible error if a call is made
|
|
|
|
C++11 allows a constructor to call peer constructors
|
|
|
|
- Allows re-use of implementation (through delegation)
|
|
- Object is fully constructed once any constructor finishes
|
|
|
|
C++11 lets you ask compiler to synthesize operations
|
|
|
|
- Explicitly, but only for basic copy control, default constructor
|
|
- End the declaration with `= default` (and don't define) The compiler will then generate the operation or throw an error if it can't.
|
|
|
|
## Shallow vs Deep Copy
|
|
|
|
### Shallow Copy Construction
|
|
|
|
```cpp
|
|
// just uses the array that's already in the other object
|
|
IntArray::IntArray(const IntArray &a)
|
|
:size_(a.size_),
|
|
values_(a.values_) {
|
|
// only memory address is copied, not the memory it points to
|
|
}
|
|
|
|
int main(int argc, char * argv[]){
|
|
IntArray arr = {0,1,2};
|
|
IntArray arr2 = arr;
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
There are two ways to "copy"
|
|
|
|
- Shallow: re-aliases existing resources
|
|
- E.g., by copying the address value from a pointer member variable
|
|
- Deep: makes a complete and separate copy
|
|
- I.e., by following pointers and deep copying what they alias
|
|
|
|
Version above shows shallow copy
|
|
|
|
- Efficient but may be risky (why?) The destructor will delete the memory that the other object is pointing to.
|
|
- Usually want no-op destructor, aliasing via `shared_ptr` or a boolean value to check if the object is the original memory allocator for the resource.
|
|
|
|
### Deep Copy Construction
|
|
|
|
```cpp
|
|
IntArray::IntArray(const IntArray &a)
|
|
:size_(0), values_(nullptr) {
|
|
|
|
if (a.size_ > 0) {
|
|
// new may throw bad_alloc,
|
|
// set size_ after it succeeds
|
|
values_ = new int[a.size_];
|
|
size_ = a.size_;
|
|
|
|
// could use memcpy instead
|
|
for (size_t i = 0;
|
|
i < size_; ++i) {
|
|
values_[i] = a.values_[i];
|
|
}
|
|
}
|
|
}
|
|
int main(int argc, char * argv[]){
|
|
IntArray arr = {0,1,2};
|
|
IntArray arr2 = arr;
|
|
return 0;
|
|
}
|
|
|
|
```
|
|
|
|
This code shows deep copy
|
|
|
|
- Safe: no shared aliasing, exception aware initialization
|
|
- But may not be as efficient as shallow copy in many cases
|
|
|
|
Note trade-offs with arrays
|
|
|
|
- Allocate memory once
|
|
- More efficient than multiple calls to new (heap search)
|
|
- Constructor and assignment called on each array element
|
|
- Less efficient than block copy
|
|
- E.g., using `memcpy()`
|
|
- But sometimes necessary
|
|
- i.e., constructors, destructors establish needed invariants
|
|
|
|
Each object is responsible for its own resources.
|
|
|
|
## Swap Trick for Copy-Assignment
|
|
|
|
The swap trick is a way to implement the copy-assignment operator, given that the `size_` and `values_` members are already defined in constructor.
|
|
|
|
```cpp
|
|
class Array {
|
|
public:
|
|
Array(unsigned int) ; // assume constructor allocates memory
|
|
Array(const Array &); // assume copy constructor makes a deep copy
|
|
~Array(); // assume destructor calls delete on values_
|
|
Array & operator=(const Array &a);
|
|
private:
|
|
size_t size_;
|
|
int * values_;
|
|
};
|
|
|
|
Array & Array::operator=(const Array &a) { // return ref lets us chain
|
|
if (&a != this) { // note test for self-assignment (safe, efficient)
|
|
Array temp(a); // copy constructor makes deep copy of a
|
|
swap(temp.size_, size_); // note unqualified calls to swap
|
|
swap(temp.values_, values_); // (do user-defined or std::swap)
|
|
}
|
|
return *this; // previous *values_ cleaned up by temp's destructor, which is the member variable of the current object
|
|
}
|
|
|
|
int main(int argc, char * argv[]){
|
|
IntArray arr = {0,1,2};
|
|
IntArray arr2 = {3,4,5};
|
|
arr2 = arr;
|
|
return 0;
|
|
}
|
|
|
|
```
|
|
|
|
## Review: Construction/destruction order with inheritance, copy control with inheritance
|
|
|
|
### Constructor and Destructor are Inverses
|
|
|
|
```cpp
|
|
IntArray::IntArray(unsigned int u)
|
|
: size_(0), values_(nullptr) {
|
|
// exception safe semantics
|
|
values_ = new int [u];
|
|
size_ = u;
|
|
}
|
|
|
|
IntArray::~IntArray() {
|
|
|
|
// deallocates heap memory
|
|
// that values_ points to,
|
|
// so it's not leaked:
|
|
// with deep copy, object
|
|
// owns the memory
|
|
delete [] values_;
|
|
|
|
// the size_ and values_
|
|
// member variables are
|
|
// themselves destroyed
|
|
// after destructor body
|
|
}
|
|
```
|
|
Constructors initialize
|
|
|
|
- At the start of each object's lifetime
|
|
- Implicitly called when object is created
|
|
|
|
Destructors clean up
|
|
|
|
- Implicitly called when an object is destroyed
|
|
- E.g., when stack frame where it was declared goes out of scope
|
|
- E.g., when its address is passed to delete
|
|
- E.g., when another object of which it is a member is being destroyed
|
|
|
|
### More on Initialization and Destruction
|
|
|
|
Initialization follows a well defined order
|
|
|
|
- Base class constructor is called
|
|
- That constructor recursively follows this order, too
|
|
- Member constructors are called
|
|
- In order members were declared
|
|
- Good style to list in that order (a good compiler may warn if not)
|
|
- Constructor body is run
|
|
|
|
Destruction occurs in the reverse order
|
|
|
|
- Destructor body is run, then member destructors, then base class destructor (which recursively follows reverse order)
|
|
|
|
**Make destructor virtual if members are virtual**
|
|
|
|
- Or if class is part of an inheritance hierarchy
|
|
- Avoids “slicing”: ensures destruction starts at the most derived class destructor (not at some higher base class)
|