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.
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.
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.
- 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?
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.
- 1Identify the copy operation: is a new object being initialized from an existing one, or is an existing object being overwritten?
- 2Find the matching member in the class declaration: the copy constructor for initialization, the copy assignment operator for overwriting.
- 3Read the parameter and return type of that member to confirm what it accepts and what it produces.
- 4Trace 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.
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.
- 1Create 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.
- 2Use 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.
- 3Leave 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.
- 4Release the resource
The destructor ~
Vector()returns the array held in elem to the free store with delete[]. Nothing from the acquisition is left behind.
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.
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
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.
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.
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 likeAssuming that Vector b = a; and b = a; both run the same function
Why it failsThe first initializes a new object, so the copy constructor runs. The second overwrites an existing object, so the copy assignment runs.
Do this insteadCheck 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 likeWriting v.~Vector(); and then letting v go out of scope
Why it failsUsers 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 insteadLet 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 failsThe 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 insteadWhen 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 failsNaked new and naked delete scattered through general code make it easy to forget a release and leak the resource.
Do this insteadKeep acquisition in a constructor and release in a destructor, as RAII recommends.
Practice Tracing Copy and Lifetime
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.