Files
NoteNextra-origin/content/CSE332S/CSE332S_L14.md
2025-07-06 12:40:25 -05:00

7.1 KiB

CSE332S 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

// 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

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.

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

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)