Part C - Encapsulation

Classes and Resources

Add resources to a compound type
Describe the copying operations for classes with resources
Define the copy constructor and assignment operator for a class

"Never allocate more than one resource in a single statement" (Sutter, Alexandrescu, 2005)

Resource Pointers | Copying Operations | Copy Constructor | Assignment Operator
Localization | No Copies | Summary | Exercise


An object can access resources outside its own region of memory.  An array allocated at run-time in dynamic memory is one example.  The copying operations built into the core language only copy the object's data members.  We call this member-wise copying shallow copying.  Shallow copying only suffices where the resource is shared by all instances of a class at all times.  Copying an object that refers to an unshared resource involves copying the resource as well.  We call this copying deep copying

This chapter describes how to implement deep copying logic.  The affected member functions are the constructors, the assignment operator and the destructor. 


Resource Instance Pointers

An object refers to a resource through a resource instance pointer.  The pointer holds the resource's address.  The memory that the resource occupies is outside the object and is independent of it. 

Consider upgrading our Student class to accomodate a variable number of grades determined at run-time.  The steps are set out in the chapter on Dynamic Memory.  We allocate

  • static memory for the resource instance variable that will hold the address of the grade string
  • dynamic memory for the elements of the grade string

The changes to our Student class are highlighted below:

 // Resources - Constructor and Destructor
 // resources.cpp

 #include <iostream>
 #include <cstring>
 using namespace std;

 class Student {
     int no;
     char* grade;
 public:
     Student();
     Student(int, const char*);
     ~Student();
     void display() const;
 };

 Student::Student() {
     no = 0;
     grade = nullptr;
 }

 Student::Student(int n, const char* g) {
     // see Current Object chapter for validation logic 
     no = n;
     // allocate dynamic memory
     grade = new char[strlen(g) + 1];
     strcpy(grade, g);
 }

 Student::~Student() {
     // deallocate previously allocated memory
     delete [] grade;
 }

 void Student::display() const {
     cout << no << ' ' <<
          ((grade != nullptr) ? grade : ""); 
 }

 int main ( ) {
     Student harry(1234, "ABACA");

     harry.display();
     cout << endl;
 }











































 1234 ABACA


The conditional expression in the display() method distinguishes between allocated and unallocated memory.  The destructor deallocates any memory that has been allocated.  Deallocating memory at the nullptr address has no effect. 

note that this example focuses on construction and destruction, but does not address copying operations. 


Copying Operations

A shallow copy copies the resource instance pointer from the source object to the recipient object, resulting in both objects pointing to the same resource.  If the client changes a value in the recipient object's resource, the value to which the source object refers is the changed value.  This behavior is desirable for copy operations only with shared resources.  With an unshared resource, the client expects the data in the resource tied to the source object to remain unaltered after altering the data in the resource tied to the recipient object. 

Ensuring that the resources tied to original and duplicate objects change independently require separate allocation of memory.  The resource instance pointer in each object points to a different location in dynamic memory. 

deep copy

In the above example, the pointer grade in the recipient object should hold a different address from the address stored in the source object. 

For each copy, we obtain a memory address for a new resource and copy the contents of the original resource into the new memory.  We shallow copy only those instance variables that are NOT resource instance variables.  For example, in our Student class, we shallow copy the student number, but not the address stored in the grade pointer.

The two member functions of a compound type that contain allocation logic are:

  • the copy constructor
  • the assignment operator

If we do not declare a copy constructor, the compiler inserts code that implements a shallow copy.  If we do not declare an assignment operator, the compiler inserts code that implements a shallow assignment. 


Copy Constructor

The copy constructor defines how to copy information from a source object to a newly created object of the same type.  This constructor is called if an object is

  1. initialized
  2. copied by value from an argument in a function call
  3. returned by value from a function

Declaration

The declaration of a copy constructor takes the form

 Type(const Type&);

where Type is the name of the compound type. 

For example, we insert the declaration into the definition of our Student class: 

 // Student.h

 class Student {
     int no;
     char* grade;
 public:
     Student();
     Student(int, const char*);
     Student(const Student&);
     ~Student();
     void display() const; 
 };

Definition

The definition of a copy constructor

  1. performs a shallow copy the non-resource instance variables
  2. allocates memory for a new resource
  3. copies data from the source resource to the newly created resource

For example, we insert the following definition into the implementation file for our Student class:

 // Student.cpp

 #include <iostream>
 #include <cstring>
 using namespace std;
 #include "Student.h"

 // ...

 Student::Student(const Student& src) {

     // shallow copy
     no = src.no;

     // allocate dynamic memory for grade string
     if (src.grade != nullptr) {
         grade = new char[strlen(src.grade) + 1]; 
         // copy data from the original resource
         // to newly allocated resource
         strcpy(grade, src.grade);
     }
     else
         grade = nullptr;
 }

Since the source data was validated when originally received from the client and privacy constraints have ensured that this data has not been corrupted in the interim, we do not need to validate the data again.


Assignment Operator

The assignment operator defines how to copy data from an existing object to another existing object.  This member operator is called on expressions of the form

 identifier = identifier

where identifier refers to the name of an object. 

Declaration

The declaration of an assignment operator takes the form

 Type& operator=(const Type&);

where the left Type is the type of the destination operand.  The right Type is the type of the source operand. 

For example, we insert the declaration into the definition of our Student class: 

 // Student.h

 class Student {
     int no;
     char* grade;
 public:
     Student();
     Student(int, const char*);
     Student(const Student&);
     Student& operator=(const Student&); 
     ~Student();
     void display() const;
 };

Definition

The definition of the assignment operator:

  • checks for self-assignment
  • shallow copies non-resource instance variables to destination variables
  • deallocates any previously allocated resource
  • allocate a new resource
  • copies resource data associated with the source object to the newly allocated memory for the resource of the recipient object

For example, we insert the following definition into the implementation file for our Student class:

 // Student.cpp

 // ...

 Student& Student::operator=(const Student& source) {

     // check for self-assignment
     if (this != &source) {
         // shallow copy non-resource variable
         no = source.no;
         // deallocate previously allocated dynamic memory
         delete [] grade;
         // allocate new dynamic memory, if needed
         if (source.grade != nullptr) {
             grade = new char[strlen(source.grade) + 1]; 
             // copy the resource data
             strcpy(grade, source.grade);
         }
         else
             grade = nullptr;
     }
     return *this;
 }

To trap self-assignments (a = a), we compare the address of the current object to the address of the source object.  If the addresses are identical, we skip the assignment logic altogether.  If we were to neglect checking for self-assignment, the deallocation statement would deallocate the memory holding the resource data, we would lose access to the resource and our logic would fail at the std::strlen call. 


Localization

The code in the definition of the copy constructor is identical to most of the code in the definition of the assignment operator.  We avoid this duplication in either of two ways:

  • localizing the common code in a private member function and calling that member function from the copy constructor and the assignment operator
  • calling the assignment operator directly from the copy constructor

Private Member Function

In the following example, the common code is in the private method named init(), which our copy constructor and assignment operator call:

 void Student::init(const Student& source) {

     no = source.no;
     if (source.grade != nullptr) {
         grade = new char[strlen(source.grade) + 1];
         strcpy(grade, source.grade);
     }
     else
         grade = nullptr;
 }

 Student::Student(const Student& source) {
     init(source);
 }

 Student& Student::operator=(const Student& source) {
     if (this != &source) {  // check for self-assignment
         // deallocate previously allocated dynamic memory 
         delete [] grade;
         init(source);
     }
     return *this;
 }

Direct Call

In the following example, we initialize the resource instance variable in the copy constructor to nullptr and then call the assignment operator:

 Student::Student(const Student& source) {
     grade = nullptr;
     *this = source; // calls assignment operator
 }

 Student& Student::operator=(const Student& source) {
     if (this != &source) {  // check for self-assignment
         no = source.no;
         // deallocate previously allocated dynamic memory
         delete [] grade;
         // allocate new freestore memory
         if (source.grade != nullptr) {
             grade = new char[strlen(source.grade) + 1]; 
             // copy resource data
             strcpy(grade, source.grade);
         }
         else
             grade = nullptr;
     }
     return *this;
 }

The initialization neutralizes the deallocation statement in the assignment operator.  Without this initialization, the deallocation would cause a serious error. 


No Copies Allowed

For certain compound types, we may chose to prohibit the client from copying any object.  To do so, we simply declare the copy constructor and the assignment operator as private members:

 class Student {
     int no;
     char* grade;
     Student(const Student& source);
     Student& operator=(const Student& source); 
 public:
     Student();
     Student(int, const char*);
     ~Student();
     void display() const;
 };

We do not need to define these private members unless we call them from other member functions.


Summary

  • class with unshared resources require definitions of a copy constructor, assignment operator and destructor
  • the copy constructor copies data from an existing object to a newly created object
  • the assignment operator copies data from an existing object to an existing object
  • initialization, pass by value, and return by value invoke the copy constructor
  • the non-resource instance variables shallow copy in a copy or assignment operation
  • check for self-assignment in the assignment operator

Exercises



Previous Reading  Previous: The Current Object Next: Member Operators   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo
Creative Commons License