# CSE332S Lecture 13 ## 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)