Concepts / Constructors and Destructors for Resource Handles

Constructors and Destructors for Resource Handles

RAII ties resource release to the lifetime of an object. Because the destructor runs as that lifetime ends, a resource acquired in a constructor is released without a separate cleanup call that could be forgotten.

  • Programming

Why a Copy Can Go Wrong

When you write a class, you usually expect that copying one of its objects will produce a sensible second object. For simple fields that expectation holds without any extra work. Many useful classes, however, do not hold all of their data in their own fields. They hold a handle, such as a pointer, that refers to a resource that lives somewhere else. Copying such an object is no longer a simple matter of copying fields, because the resource itself has to be considered as well.

This article uses a class called Vector to show how C++ handles that situation. Vector is a container, which means an object that holds a collection of elements. It stores its elements in an array obtained with new, and it releases that array in its destructor. Two members of the class, the copy constructor and the copy assignment operator, together define what a copy of a Vector contains. By the end of this article you should be able to find those two members, read their declarations, trace when the resource is acquired and released, and explain the RAII technique that ties these steps together.

Handles and the Data They Refer To

The Vector class holds two data members. The member elem is a pointer to an array of doubles, and sz records how many doubles there are. The array itself is not stored inside the Vector object. It lives on the free store, and the Vector refers to it through elem. This arrangement is called the handle-to-data model: a small object holds a pointer and a count, and the larger block of data it refers to is kept elsewhere.

The model is useful when data can change size during an object's lifetime. The Vector object stays small and fixed in size no matter how many elements it refers to, while the elements can live in a block that is sized to fit them. The handle is the small object the program actually holds. The data is the larger block the handle points to. Because the destructor knows where that block begins through elem, it can release the whole block in one step.

This is the reason copying needs attention. A copy of a Vector must deal with the array as well as with the fields. Whatever the class decides to do about that array is part of the class's copying behavior, and the class has to say what that behavior is.

Two Members That Define Copying

Copying an object of a class is defined by two members working together: a copy constructor and a copy assignment operator. Together they form the definition of copying for that class. They are the place to look whenever you want to understand what a copy of one of your objects will actually contain. The two members are separate declarations inside the class, and each one handles a different kind of copy.

Comparing the Two Copy Members

The two copy members are easy to confuse because they take the same parameter. What separates them is when each one runs. The copy constructor runs when a brand-new object is created from an existing one. The copy assignment runs when an object that already exists is overwritten with the value of another. Both members are declared inside the same class, and both determine what the copy contains.

New object fromexistingVector(const Vector&a)copy constructorExisting objectoverwrittenVector&operator=(constVector& a)copy assignmentrunsruns
Which member runs when a new object is initialized from an existing one, and which runs when an already-existing object is overwritten?

Reading the Vector Declaration

The source presents the Vector class as a list of responsibilities: private data, constructors and destructor, copy members, and element access functions. The copy members are the two lines that define copying. The listing below keeps the parts discussed here. The element access functions are omitted because they do not decide how a copy is produced.

cpp
Notes on the code
  • elem and sz are the private data: a handle and a count
  • Vector(int s) acquires the array on the free store
  • ~Vector() is the destructor, named tilde followed by the class name
  • Vector(const Vector& a) is the copy constructor: no return type, the class name itself
  • Vector& operator=(const Vector& a) is the copy assignment: returns a reference and is named operator=

Notice the two differences in the copy lines. The copy constructor has no return type and carries the class name, which marks it as a constructor. The copy assignment has a return type of Vector& and carries the name operator=, which marks it as the assignment operator. Both take a const Vector& parameter, so both read their source without modifying it.

Why Copying Must Be Stated Explicitly

Consider what would happen if a copy duplicated only the handle. The new object would receive the same pointer value as the original, and so both objects would refer to the same array on the free store. The diagram below shows that situation. It is a useful question to ask of any class that manages a resource: after a copy, what do the two objects refer to, and who is responsible for the resource when each object ends its life?

Vector aholds elem and szArray of doublesone block on the free storeVector bholds a copied elem and szelem points toelem points to
After a copy that duplicates only the handle, what do the two objects refer to, and what is at stake when each one is destroyed?

In this picture the two objects share one array. Whichever object is destroyed first would release an array the other object still refers to. The source does not describe a default copy in detail, so the diagram shows the hypothetical case that motivates the design. The practical lesson stands regardless: a class that holds a handle must say how its copies deal with the resource, and it does so in the copy constructor and copy assignment.

Locating Copy Behavior When Tracing a Bug

Suppose a program behaves unexpectedly with Vector objects after one of them has been copied. A practical first step is to ask which declaration governs the copy. Because copying is defined by the copy constructor and copy assignment, you check those two declarations in the Vector class and confirm which member the copying operation would use. The rest of the class, such as the element access functions, does not decide how the copy is produced, so it is rarely the first place to look.

  1. 1
    Identify the copy operation: is a new object being initialized from an existing one, or is an existing object being overwritten?
  2. 2
    Find the matching member in the class declaration: the copy constructor for initialization, the copy assignment operator for overwriting.
  3. 3
    Read the parameter and return type of that member to confirm what it accepts and what it produces.
  4. 4
    Trace the resource the class holds, such as elem, and confirm how the copy member handles it.

Tracing One Vector Through Its Life

The constructor and destructor work as a pair. The constructor Vector(int s) asks the free store for an array of s doubles, stores the pointer in elem, records the count in sz, and fills every slot with zero. The destructor ~Vector() hands that same array back with delete[]. Whatever the constructor takes from the free store, the destructor is responsible for returning. The timeline below follows one Vector through a single function call.

Function enteredConstructor runsVector v(3)Array of 3 doublesacquiredfrom the free storeElements usedthrough elemFunction returnsv goes out of scopeDestructor runs~Vector()Array releaseddelete[]nextacquiresnextnexttriggersreleases
In what order do acquisition and release happen as the object is created, used, goes out of scope, and is destroyed?
Worked example

A three-element Vector inside a function

A program creates a Vector with three elements inside a function, uses it, and then the function returns. Trace when the array is acquired and when it is released.

  1. 1
    Create the object

    Vector v(3) calls the constructor with s equal to 3. The constructor asks the free store for an array of 3 doubles, stores that pointer in elem, records 3 in sz, and fills the slots with zero.

  2. 2
    Use the object

    The program reads and writes elements. Those accesses go through the pointer held in elem, so the array is reached through the handle the object carries.

  3. 3
    Leave the scope

    When the function returns, the local object v goes out of scope. The user does not call anything to clean up. The object's lifetime ends, and its destructor runs.

  4. 4
    Release the resource

    The destructor ~Vector() returns the array held in elem to the free store with delete[]. Nothing from the acquisition is left behind.

Result

The array is acquired when the constructor runs and released when the destructor runs at the end of the object's lifetime. The programmer never writes the release by hand.

Users of Vector never call the destructor themselves. They create and use Vector objects much as they would variables of built-in types, and the cleanup happens without their intervention. The rules for naming, scope, allocation, and lifetime that apply to an int or a char also apply to a Vector, so the destructor runs as the object's lifetime ends.

Destructors and the Constructor Name

A destructor is the member that releases a resource. Its name is the tilde character, written ~, followed by the class name, so the destructor for Vector is ~Vector(). The name mirrors the constructor's name, which is the class name alone. In the class body, the constructor and destructor sit side by side: Vector(int s) acquires and ~Vector() releases. The tilde is the only character that distinguishes the destructor's name from the constructor's name.

Before you see the program below run, think about the order of events. The class has a constructor and a destructor, each of which prints one line. The program prints a line before an inner block, creates one object inside that block, and prints a line after the block.

cpp
Notes on the code
  • Tracer() is the constructor and runs when t is created
  • ~Tracer() is the destructor and runs when the inner block ends
  • t is created and destroyed inside the braces, so its lifetime is that block

What do you think happens?

In what order do the four lines print?

Reveal answer
Answer:

before block, constructing, destructing, after block

The first line prints before the inner block. Creating t runs the constructor, which prints constructing. Leaving the inner block ends t's lifetime, so the destructor runs and prints destructing. The last line prints after the block has closed.

Resource Acquisition Is Initialization

The constructor and destructor combination is the basis of most C++ general resource management techniques. The best-known of these is Resource Acquisition Is Initialization, usually abbreviated RAII. RAII is the technique of acquiring a resource in a constructor and releasing it in a destructor. The name describes the core idea: the act of acquiring the resource happens as part of initializing an object.

new callConstructor acquiresCode using theresourceCode using the objectdelete callDestructor releasesthenthenthenon scope exit
How does tying release to scope exit prevent a leak compared with manually paired acquire and release calls?

The before column shows acquisition and release written as two separate calls in general-purpose code. Each call has to be remembered and paired correctly by the programmer. The after column shows the same resource handled by a class. The constructor acquires it, and the destructor releases it when the object's lifetime ends. The release is attached to the object's scope, so it does not depend on the programmer remembering to write it.

The payoff is that RAII lets you eliminate what the source calls naked new operations and naked delete operations. A naked new is an allocation sitting in general-purpose code. A naked delete is a release written by hand in that same code. RAII keeps allocations buried inside the implementation of a well-behaved abstraction, such as Vector, so that everyday code does not have to remember to clean up. Avoiding naked new and naked delete makes code far less error-prone and far easier to keep free of resource leaks.

Key point

RAII ties resource release to the lifetime of an object. Because the destructor runs as that lifetime ends, a resource acquired in a constructor is released without a separate cleanup call that could be forgotten.

Mistakes Beginners Make

  • Common mistake

    Treating the copy constructor and copy assignment as the same member

    What it looks like

    Assuming that Vector b = a; and b = a; both run the same function

    Why it fails

    The first initializes a new object, so the copy constructor runs. The second overwrites an existing object, so the copy assignment runs.

    Do this instead

    Check whether the target object already exists. New objects use the copy constructor, and existing objects use the copy assignment operator.

  • Common mistake

    Calling the destructor by hand to free a Vector

    What it looks like

    Writing v.~Vector(); and then letting v go out of scope

    Why it fails

    Users of Vector never call the destructor themselves. Calling it manually while the object is still alive would release the array while the object still refers to it.

    Do this instead

    Let the object's lifetime end. The destructor runs automatically at that point.

  • Common mistake

    Assuming the element access functions decide how copies behave

    Why it fails

    The source states that copying is defined by the copy members, and the rest of the class does not decide how the copy is produced.

    Do this instead

    When tracing a copy problem, check the copy constructor and copy assignment declarations first.

  • Common mistake

    Writing new and delete in ordinary code instead of inside a class

    Why it fails

    Naked new and naked delete scattered through general code make it easy to forget a release and leak the resource.

    Do this instead

    Keep acquisition in a constructor and release in a destructor, as RAII recommends.

Practice Tracing Copy and Lifetime

MEDIUM

A program copies a Vector and then behaves unexpectedly with the two objects. Using only the Vector class declaration, explain which member you would check first to determine what the copy contains, and say which member runs if the copy initializes a brand-new object versus overwrites one that already exists. Then trace when the array is acquired and when it is released for an object created in a block.

Hints
  • Start from the copy operation and ask whether a new object is initialized or an existing one is overwritten.
  • Find the copy constructor for initialization and the copy assignment operator for overwriting.
  • The array is acquired by the constructor and released by the destructor at the end of the object's lifetime.

Key Takeaways

  • Copying an object of a class is defined by two members: the copy constructor, which initializes a new object from an existing one, and the copy assignment operator, which overwrites an existing object.
  • A class that holds a handle to a resource outside its own fields must state its copying behavior explicitly, and the copy members are the place to look when tracing a copy problem.
  • A constructor acquires a resource such as an array allocated with new, and the destructor, named tilde followed by the class name, releases it with delete.
  • Resource Acquisition Is Initialization (RAII) keeps acquisition in constructors and release in destructors, which lets code avoid naked new and naked delete and reduces resource leaks.

Related concepts

More concepts starting with C