Lesson #9

Resource Management

Overview

In this lesson I will cover the following topics

  1. The difference between initialization and assignment.

  2. How and why you should define a copy constructor and a copy assignment operator for a class. (This is done to manage resources that an object owns that are not actually stored inside the object itself).

  3. How objects are often copied more than you might expect and how the compiler insures that all such copies are properly made and cleaned up.

Body

Initialization vs assignment.

There is an important difference between initializing an object and assigning to an object. When you initialize an object, its value is at first meaningless. The purpose of the initialization is to give the object a meaningful value. However, when you assign a value to an object, the object has a meaningful value already. That value is getting overwritten with a new value.

This distinction is subtle. In fact, with simple types like integers, you don't have to think about it at all. For example, what's the difference between

int x;
x = 10;

and

int x = 10;

There really isn't much of a difference. In the first case, x is not initialized, but later I assign it a value. When I do the assignment, I overwrite the indeterminate value that was initially in x. In the second case, I initialize x right away, but otherwise it's hard to see what the difference is.

For complicated types, there can be a BIG difference.

BigInt number;
number = 10;

Here I first execute the default constructor in class BigInt to initialize number in some sensible way. In general, class objects need to be initialized before they will even "work." The purpose of the default constructor is to ensure that this internal set-up gets done. Yet in my example, just after initializing the object to a default value, I then overwrite that value by assigning 10 to the object. The work done initializing the object to the default value was a waste.

BigInt number = 10;

Here I invoke the BigInt constructor that takes an integer parameter. That constructor initializes the BigInt to the given value directly. Since it doesn't bother doing the work of the default constructor (just to undo it later) it is faster.

Remember: initialization is usually faster because it does not have to undo the value currently stored in an object. For speed reasons, you should make every effort to initialize all objects to useful values when you declare them. To support this practice, C++ allows you to declare objects anywhere in your program. You don't need to put all your declarations at the top of each block the way you do in C 1990 (that restriction was lifted in C99).

Objects that hold dynamic memory.

This is a very important example, so study it closely.

Many useful objects need to manage dynamic memory internally. Typically, they allocate some memory when they are constructed and release that memory when they are destroyed. While they are being used, they often reallocate their internal memory to cope with the demands of the user. Here is a skeleton example:

class Skeleton {
public:
    Skeleton( );  // Constructor allocates some memory.
   ~Skeleton( );  // Destructor releases it.

    // Other public operations of interest...

private:
    char *workspace;
};

Here is how the constructor and destructor work:

Skeleton::Skeleton( )
{
    workspace = new char[1024];
      // Allocate 1024 bytes to start. I might expand this later in
      // the other methods, depending on what I'm asked to do.
}


Skeleton::~Skeleton( )
{
    delete [] workspace;
      // Get rid of whatever dynamic memory I've allocated during my lifetime.
}

When a user creates a Skeleton object, its default constructor will allocate some memory behind the scenes. When that Skeleton object goes away, the compiler will call its destructor function automatically, and it will release the dynamic memory by itself. The user does not need to concern themself with deleting the dynamic memory. Memory leaks are avoided because for every Skeleton created, its destructor will definitely be called when it is no longer needed.

In general, this is how all resources are handled. An object obtains the resources it needs when it is constructed (or during its lifetime on demand) and then releases those resources when it is destroyed. While memory is a very important resource that many objects need, there are other resources that need managing too. Examples of such resources include: open files, network connections, database locks, graphical contexts, subordinate threads, etc. Failure to make sure that every resource acquired by the program is ultimately released is a major source of program bugs. In C++, these problems can be minimized by using the "resource allocation is initialization" (RAI) idiom. In other words: allocate your resources as part of the initialization (construction) of some object and release those resources in the corresponding destruction.

Copying objects.

C++ allows you to assign one class object to another. This is consistent with the way C allows you to assign one structure to another with an equals sign. When you do assign one class object to another, each member is assigned one-for-one. This is often what you want. Consider the class Date I discussed earlier. If I do:

vtsu::Date X, Y;

X = Y;
  // Assign all the members of Y to the corresponding members of X.

the member-for-member assignment is perfect. I want the day, month, and year members of Y to be put into the corresponding members of X.

But what happens if you try to assign one Skeleton object to another?

vtsu::Skeleton X, Y;
  // Each of these objects holds a pointer to 1024 bytes of
  // dynamically allocated memory.

X = Y;
  // Copy the pointer in Y to X. Bad!

The address stored in Y.workspace is copied to X.workspace. This means that two different Skeleton objects will point at the same dynamic memory. It also means that the dynamic memory X.workspace once pointed at no longer has anything pointing at it. The object X has leaked memory! What's worse, when X and Y are destroyed, a delete operation will be done on the same block of memory twice. That is caused undefined behavior and is very bad.

Here is a picture of X and Y before the assignment is done:

Object X has a single member named "workspace."
+-----------+
| workspace |------> (Block of 1024 bytes of memory)
+-----------+

Object Y has a single member named "workspace."
+-----------+
| workspace |------> (Block of 1024 bytes of memory)
+-----------+

After doing X = Y I have:

Object X
+-----------+
| workspace |--------+    (Block of 1024 bytes of memory)
+-----------+        |
                     |
Object Y             |
+-----------+        V
| workspace |------> (Block of 1024 bytes of memory)
+-----------+

The address stored in Y.workspace has been put into X.workspace so now both pointers point at the same block of memory. The block of memory that X.workspace used to point at is not inaccessible (and can never be deleted).

This situation is bad, but there is a way to fix it.

C++ allows you to overload the assignment operator. In this way, you can define what it means to assign one object to another and do whatever it takes to make a proper assignment.

class Skeleton {
public:
    Skeleton( );
   ~Skeleton( );

    Skeleton &operator=( const Skeleton &right );
      // The assignment operator should always be declared like this.

    // Other public operations of interest...

private:
    char *workspace;
};

I've modified the definition of class Skeleton to include an overloaded operator= function. The way I have declared that function is traditional and highly recommended. It should take its right parameter by reference (specifically, a reference to a const since assignment should not change the source object). It should return a reference so that assignments can be chained in the usual way.

X = Y = Z;
  // Really X.operator=( Y.operator=( Z ) );

Here is what Skeleton's operator= looks like:

Skeleton &Skeleton::operator=( const Skeleton &right )
{
    if( this == &right ) return *this;
      // If I'm trying to assign myself to myself, just return
      // a reference to myself. This is a common optimization.

    delete [] workspace;
      // Free my current workspace so that I don't leak memory.

    workspace = new char[1024];
      // Make a new workspace into which I will copy the incoming
      // workspace. Here I'm just using a size of 1024, but in
      // real life I would have to find out how big right's workspace
      // is and use that size here. (One assumes that right has
      // expanded its workspace since it was first constructed).

    std::memcpy(workspace, right.workspace, 1024);
      // Copy right's workspace into my new one.

    return *this;
      // This strange looking expression is how you can return a
      // reference to yourself. You should always do this at the
      // end of your assignment operations.
}

Instead of just copying right's workspace member, which would be wrong, this function creates a new workspace value and copies the real data that right is holding. Now when I do X = Y everything works. X does not leak memory and Y's data is properly copied into X. Pretty cool, huh?

Notice that my copy assignment operator specifically checks to see if the source of the assignment is the same as the target. In that special case, it does nothing. This step is important because many copy assignment operators will malfunction in a self-assignment context. For example, Skeleton's copy assignment above, first deallocates the target's workspace. However, if that target and the source are the same, that deallocates the source's workspace as well, leaving us with no workspace at all!

Even in cases where there is no problem in theory with self-assignment, it can still entail a lot of extra copying which is inefficient. For this reason, it is a best practice to check for self-assignment right away as shown in this example.

You might wonder why anyone would purposely assign and object to itself. It is true that programmers do not normally write code such as X = X. However, self-assignment can arise in less obvious ways. For example, consider this function that fills an array of integers with a value provided indirectly via a pointer:

void fill( int *array, int size, const int *value )
{
    for( int i = 0; i < size; ++i )
        array[i] = *value;
}

Now consider this code that uses the fill function:

int main( )
{
    int array[10];

    fill( array, 10, &array[7] );
      // Fill the array with copies of array[7].

In the process of executing the fill, one of the loop iterations will self-assign array[7]. However, this is not obvious by looking at the fill function definition. Self-assigning an integer is harmless. You should make sure your copy assignment operators are also harmless in this case.

The copy assignment operator I show above still has a problem: it is not exception safe. Exception safety is a big topic. I discuss exception safety in detail in Lesson 19.

There is another context where objects get copied: initialization. Consider this example

Skeleton X;
Skeleton Y = X;
  // Here I'm using X to initialize Y. I could have also written this as
  // "Skeleton Y( X )" if I wanted to use the function-like initialization syntax.

As with assignment, the compiler will automatically initialize Y's members with the corresponding members of X (actually, the rules are a bit more complicated than this, but we don't need to fuss with all those details right now). While this is fine for some types (like Date), it is no good for Skeleton for exactly the same reasons I explained above.

To fix this, you can define a copy constructor that the compiler will use when copying an object like this. Here is Skeleton's definition again.

class Skeleton {
public:
    Skeleton( );
   ~Skeleton( );
    Skeleton &operator=( const Skeleton &right );

    Skeleton( const Skeleton &other );
      // The copy constructor should always be declared like this.

    // Other public operations of interest...

private:
    char *workspace;
};

The copy constructor is a constructor that takes a reference to the same type as it is constructing. It is used by the compiler to initialize one object with another of the same type. Here is what Skeleton's copy constructor looks like:

Skeleton::Skeleton( const Skeleton &other )
{
    workspace = new char[1024];
      // As before, I should really find out how big other's workspace
      // is right now and use that size.

    std::memcpy(workspace, other.workspace, 1024);
}

Compare this with Skeleton's operator=. The issues are the same, but there are some important differences.

First, I did not delete workspace at first. Why not? Because when an object is being constructed its members have undefined values. If I attempted to do delete [] workspace in this constructor I would be attempting to delete a random piece of memory!

The assignment operator must clean up the object's current value (to avoid leaks), the copy construct must not attempt to clean up the current value (because the object has no current value). This is the essential difference between initialization and assignment. This is why initialization uses a different function than assignment. If you are unclear about this, go back and re-read my earlier section on the difference between those two concepts. Remember: in C, the difference is irrelevant. In C++ it is critical.

Second, I did not bother to return *this in the copy constructor. That's because the constructor does not return anything.

Because the copy constructor does not have to bother cleaning up the object's existing value, it has less to do than the assignment operator. Consequently, it is normally faster. Remember: Initialization is usually faster than assignment. Initialize all your objects when you declare them!

Finally, I did not need to deal with self-assignment. An object can't be used until after it has been declared. The copy constructor executes as part of the declaration. There is no way to initialize an object with itself.

!!! NOTE !!!

Whenever you create a class that does dynamic memory allocation internally (and many classes do this), you will need:

  1. A destructor to release the dynamic memory when the object is no longer needed.

  2. An assignment operator to properly copy the dynamic memory and release any that is no longer needed when one object is assigned to another.

  3. A copy constructor to properly initialize the dynamic memory of one object with that of another.

You either need all three of these methods, or you don't need any of them. It is quite unusual to have a class that needs only one or two of these methods. If you think you have such a class, check carefully. You might find a bug. You can use my Skeleton class as a skeleton for how these functions should be declared and for what they must do.

More copying than meets the eye!

Actually, objects get copied around in your program more often than you might think. Take a look at this example using integers:

int f( int x )
{
    x++;
    return x;   // Initialize anonymous temporary in caller by copying x.
                // Destroy x, and then return.
}

int main( )
{
    int y = 1;
    int z;

    z = f( y ); // Initialize x (parameter) by copying y.
                // When function returns assign anonymous temporary holding result to z.
                // Destroy anonymous temporary.
    return 0;
}

This program is pretty silly, but it illustrates my point. When I do f( y ) the value of the argument y is copied into the parameter of the function. That's the way C and C++ functions work: the parameters are copies of the arguments. Inside the function, the parameter is incremented, and then it is copied back to the calling function. That copy goes into an anonymous temporary object in the calling function. Then, finally, the anonymous temporary is assigned to z.

The fact that the return value of the function first goes into an anonymous temporary may seem strange. Why not just put it directly into z? In this case, that would make sense. But the function does not know what is going to happen to the return value, so it has to consider that. Take a look at this usage:

int main( )
{
    int y = 1;
    int z;

    z = f( y ) + 10;
    return 0;
}

Obviously, here the return value of the function is not to be put directly into z. Where should it go? It has to participate in a larger expression. The function (which does the copying) just assumes it is initializing an anonymous temporary. It's up to the calling program to deal with the value from then on.

If all this sounds quite complicated... it is! When you first learned C, you probably didn't think much about anonymous temporaries and so forth. That's because when the types are simple, like int, the whole issue pretty much doesn't matter—unless you are trying to write a compiler. But in C++, copying an object might be an ordeal. Some objects are huge and have extremely complicated, time-consuming copy constructors. The idea of copying things around a lot should make you cringe. If it doesn't, you haven't been programming in C++ long enough yet!

To explore this idea more fully, I created a special Probe class. This class does nothing important, but it does have all the critical lifecycle functions. It has a default constructor, a destructor, a copy assignment operator, and a copy constructor. Each probe object gets a unique identification number (the constructors take care of that), and all the functions print out a message telling you what they are doing. Here is what Probe.hpp looks like:

class Probe {
public:
    Probe( );
    Probe( const probe & );
    Probe &operator=( const probe & );
   ~Probe( );

private:
    int ID_number;
};

This is basically the same as Skeleton's definition. Here is what Probe.cpp looks like:

static int master_ID = 0;

Probe::Probe( )
{
    ID_number   = ++master_ID;
    std::cout << "Default constructor: ID = " << ID_number << "\n";
}

Probe::Probe( const Probe &existing )
{
    ID_number = ++master_ID;
    std::cout << "Copy constructor: ID = " << ID_number << ". Copying object "
              << existing.ID_number << "\n";
}

Probe &Probe::operator=( const Probe &other )
{
    std::cout << "Assigning to " << ID_number << " from "
              << other.ID_number << "\n";
    return *this;
}

Probe::~Probe( )
{
    std::cout << "Destructor: " << ID_number << "\n";
}

Study this over and make sure you understand what is going on here. To exercise this class, I wrote a simple test program. Here it is:

Probe global_object;
  // Gets constructed before main is called and then destroyed when main returns.

Probe f( Probe incoming )
{
  // Create a local probe. Causes default construction.
  Probe result;

  result = incoming;
    // Assign incoming to result.

  // Copy construct this probe into a temporary in the calling program's
  // address space. Destroy this local probe. Also destroy the parameter.
  //
  return result;
}

int main( )
{
  // Create a local Probe. Causes default construction.
  Probe answer;

  // Assign temporary to `answer` and then destroy the temporary.
  answer = f( answer );

  // Destroy the local probe declared in this function.
  return 0;
}

There is a lot going on here. Let me spell it all out.

  1. First global_object is constructed. This happens before main starts.
  2. Function main begins and answer is constructed.
  3. The copy constructor is used to initialize f's parameter.
  4. Function f is called and result is constructed.
  5. Function f's parameter is assigned to result.
  6. The copy constructor is used to copy result back to an anonymous temporary object in main.
  7. result is destroyed.
  8. Function f returns and the parameter is destroyed.
  9. The anonymous temporary is assigned to answer.
  10. The anonymous temporary is destroyed.
  11. answer is destroyed.
  12. Function main returns and global_object is destroyed.

Despite the apparent simplicity of the program, the compiler is calling numerous functions to initialize, copy, and clean up the objects involved. While this may seem inefficient, it is actually very nice. Once you define the appropriate functions for a class, the objects more or less take care of themselves. Notice how even the anonymous temporary object is cleaned up with the destructor. That is just as it must be if all memory leaks are to be avoided.

Let me run this program and see what it does. I get this output:

Default constructor: ID = 1
Default constructor: ID = 2
Copy constructor: ID = 3. Copying object 2
Default constructor: ID = 4
Assigning to 4 from 3
Copy constructor: ID = 5. Copying object 4
Destructor: 4
Destructor: 3
Assigning to 2 from 5
Destructor: 5
Destructor: 2
Destructor: 1

Be sure you understand what is happening during each line above.

Summary

  1. Here is a skeleton class that you could use as the basis for any class that manages a resource.

         class Skeleton {
         public:
             Skeleton( );
               // The default constructor gives meaningful values to the pri-
               // vate data members and acquires any system resources the
               // object is going to need (such as dynamically allocated
               // memory).
    
            ~Skeleton( );
               // The destructor releases any system resources the object
               // has been using so that the program won't leak those
               // resources.
    
             Skeleton( const Skeleton &existing );
               // The copy constructor initializes the members of one object
               // with a copy of an existing object. Any system resources
               // the existing object is using will need to be copied
               // properly. Just copying members is probably not enough.
    
             Skeleton &operator=( const &Skeleton right );
               // The assignment operator makes a copy of its right operand.
               // However, unlike the copy constructor it first has to
               // "clean up" the value currently stored in the left
               // operand (the implicit object). Assignment is somewhat
               // like a destruction of the left operand followed by
               // a copy construction. However, you must explicitly
               // code the necessary steps.
    
         private:
             // The data members required to make the class work.
         };
    
  2. If the class manages resources that exist outside the data members themselves (like dynamic memory, files, network connections, etc.), then it will almost certainly need all the above methods.

  3. If the class does not manage any outside resources, it probably won't need the destructor, copy constructor, or assignment operator.

  4. Initialization and assignment are different. When an object is initialized (constructed) it has no current value, so you must not attempt to clean up its current value in the copy constructor. On the other hand, you must clean up its current value in the assignment operator before giving it a new value in order to avoid resource leaks.

  5. You should try to initialize objects when you declare them. C++ helps you do this by allowing you to declare objects anywhere in your program and not just at the top of a block. This practice will improve the performance of your programs since copy construction is typically faster than default construction plus a later assignment.

  6. If the destructor and/or copy constructor are defined, the compiler will use them in all relevant contexts. The copy constructor will be used to copy an object into a function parameter and to copy the return value back to the calling function. The destructor will be used to destroy the function parameter and to destroy any anonymous temporary objects created by the compiler.

© Copyright 2023 by Peter Chapin.
Last Revised: August 5, 2023