Concepts / Resource Management in Classes

Resource Management in Classes

When you write one of these functions yourself, re-check the others. Do not assume that a compiler-generated version of a function you did not write is still there and still does what your class needs.

  • Programming

A Class That Takes Ownership of Something

A class often needs something from outside itself to do its job. That something might be memory, a file, a lock, or some other limited resource. When a constructor obtains such a resource, it takes on a responsibility: the resource has to be given back at some point, and the class is the natural place for that to happen. Resource management in C++ is largely about meeting that responsibility reliably, including on code paths a programmer might not expect.

What do you think happens?

Will the compiler supply an ordinary constructor for a class you write with no constructor?

  • Yes, always

  • No, it does not generate an ordinary constructor

  • Only if the class has a pointer member

Reveal answer
Answer:

No, it does not generate an ordinary constructor. The compiler can generate implementations of several special member functions as needed, but an ordinary constructor is the exception.

This is why the practical question is not whether a class has a member function at all, but whether each needed special member function has been explicitly defined.

Special Member Functions the Compiler Can Generate

Special member functions are the functions a class can have that govern how its objects are created, copied, moved, and destroyed. The source names six of them: the default constructor, the destructor, the copy constructor, the copy assignment operator, the move constructor, and the move assignment operator.

  • Default constructor
  • Destructor
  • Copy constructor
  • Copy assignment operator
  • Move constructor
  • Move assignment operator

The compiler can generate default implementations for these functions as needed, so you do not always have to write each one yourself. There is one important exception: the compiler does not generate an ordinary constructor for you. A class that needs a constructor to set up its state must provide one.

How Explicit Definitions Change the Defaults

Defining some of these functions yourself affects which other defaults the compiler will produce. The source states that explicitly defining some defaults prevents other default definitions from being generated. So the question to ask is not simply whether a class has a given member function, but whether each special member function the class actually needs has been explicitly defined.

Key point

When you write one of these functions yourself, re-check the others. Do not assume that a compiler-generated version of a function you did not write is still there and still does what your class needs.

Constructors That Acquire and Destructors That Release

Think of a resource as having two boundaries. The constructor marks the start: this is where the object takes control of the resource. The destructor marks the end: this is where the object gives the resource back. Between those two points, the object can use the resource freely. The source's guideline is direct: if a constructor acquires a resource, the class needs a destructor to release it. Without that destructor, nothing in the class's own code returns the resource, and it stays held after the object that needed it is gone.

The following class was written for this article. Its constructor acquires an array on the heap, and its destructor releases it. Notice that the acquisition happens in the constructor, so the release belongs in the destructor.

Code walkthrough
class ResourceBuffer {
public:
    ResourceBuffer(int n) : size_(n), data_(new int[n]) {
    }
    ~ResourceBuffer() {
        delete[] data_;
    }
    int size() const { return size_; }
private:
    int size_;
    int* data_;
};
  1. 1
    The constructor initializes size_ and allocates an array of n ints on the heap, storing its location in data_. This is the start boundary.
  2. 2
    The destructor runs when the object ends, and delete[] gives the array back. This is the end boundary.
  3. 3
    Code that uses a ResourceBuffer never has to call delete itself. The release is attached to the object's end.
  • The constructor acquires the heap array with new[]
  • The destructor releases the same array with delete[]

Tracing a Buffer's Lifetime

Follow one ResourceBuffer through its life. Each stage below corresponds to one point on the timeline. The important observation is that the release is tied to the object's end rather than to a line the programmer might skip.

Constructor runsresource acquiredObject in usecode uses resourceScope exitsnormal or early exitDestructor runsresource releasedthenthenalways
When is the resource acquired and released as an object is created, used, and leaves scope?

Read the timeline from left to right. The object is created, the constructor acquires the array, and the code works with it. When the scope ends, whether by reaching its closing brace or by leaving early, the destructor runs and releases the array. The programmer does not write a separate release step at the point of exit, which is why an early exit does not leave the array held.

Pointer Members and Default Copying

A pointer or reference member calls for careful, explicit decisions about copying and moving. The source advises considering copy and move behavior in light of ownership, and documenting any resource that is owned outside the class. The reason is that the compiler's default copy copies the pointer value itself, not the block of memory the pointer refers to.

cpp
Notes on the code
  • The copy shares one heap array between two objects
  • The second delete[] targets memory that was already released

After the default copy runs, two objects point to the same heap block. Each object's destructor then tries to delete that block. The first delete releases the resource, and the second acts on memory that has already been given back. This is the double delete problem that a careful copy decision is meant to prevent.

Object apointer memberHeap blockone resourceObject b (copy)copied pointerpoints topoints to
After the default copy, how many objects point to the same heap block, and what happens when each destructor runs?

Owned Versus Externally Owned Pointers

Not every pointer member is the same. Some point to a resource the class must delete, and some point to memory that lives outside the class and is managed by someone else. Before deciding how a class should copy, you have to know which kind each pointer is. The table below contrasts the two cases.

QuestionPointer to a resource the class must deletePointer to externally owned memory
Who deletes the memory?The class, in its destructorSomeone outside the class
Is default copying safe?Not safely; two objects would each try to delete the blockCan be reasonable, because the class does not release the memory
What should the code do?Make an explicit copy or move decision based on ownershipDocument that the resource is externally owned

Deciding how a pointer member affects default copying

Why Naked new and delete Are Risky

A naked new or delete is one that appears directly in ordinary code, with nothing managing the resource it produces. Every naked new must be matched by a delete somewhere else, and the programmer must make sure that match happens on every path through the code. The source explains that this burden is easy to underestimate, which is why the guideline is to avoid naked new and delete operations.

new allocationprogrammer owns itHandle acquiresresourcehandle owns itCode uses pointerCode uses handleEarly returndelete may be skippedHandle destroyedresource releasedthenthenthenthen
How do ownership and cleanup differ between a raw new and delete pair and a handle that releases the resource automatically?
  • Common mistake

    Assuming the programmer's own delete will always run

    What it looks like

    A function allocates with new, then returns early before reaching the matching delete.

    Why it fails

    A naked delete is only useful if the code path reaches it. Every path through the code has to be checked.

    Do this instead

    Attach the release to an object that cleans up automatically, such as a resource handle.

  • Common mistake

    Writing a constructor that acquires a resource but no destructor

    What it looks like

    A constructor calls new[] and the class has no destructor.

    Why it fails

    Nothing in the class's own code returns the resource, so it stays held after the object is gone.

    Do this instead

    Write a destructor that releases the resource the constructor acquired.

RAII and Resource Handles

The source recommends using resource handles and RAII to manage resources. RAII stands for Resource Acquisition Is Initialization. The name describes the pattern: a resource is acquired during the initialization of an object, and because of that, the object's lifetime controls the resource. The object's destructor performs the release.

A resource handle is an object whose job is to hold a resource on behalf of the code that uses it. The handle acquires the resource when it is created and releases it when it is destroyed. The programmer's attention can therefore stay on the work rather than on bookkeeping.

Data flows in two directions through a handle. Requests and values travel from the calling code into the handle, which forwards them to the resource. Results travel back the same way. The calling code never needs to know when or how the resource is returned, because the handle owns that decision.

Initializer-List Constructors for Containers

A container is a class that holds a collection of elements. The source says that a container class should be given an initializer-list constructor. This kind of constructor accepts a list of values written in braces, so a user can create the container already filled with those values. This makes the container easier to create and read, and it matches the way programmers commonly write small collections of values.

cpp
Notes on the code
  • The braced list is accepted by the initializer-list constructor
  • The elements are placed into the container's storage at creation

In this sketch, the container's elements come straight from the braced list, so the object is populated at the moment it is created. The source does not describe further details of this constructor, so the example shows only the general shape.

Practice: Tracing Ownership

MEDIUM

A class's constructor allocates an array with new[] and stores the pointer in a member. Which special member function must release that array, and what goes wrong if the class uses the compiler's default copy and both objects are destroyed?

Hints
  • Look for the function that marks the end of the object's life
  • Think about what the default copy duplicates: the pointer or the array
  • Ask how many destructors will try to release the same block

Key Takeaways

  • The compiler can generate default special member functions as needed, but it does not generate an ordinary constructor, so a needed constructor must be written explicitly.
  • A constructor that acquires a resource creates a duty: the class needs a destructor that releases it, and RAII ties the resource's lifetime to the object's lifetime.
  • The compiler's default copy duplicates a pointer, so a class that owns a heap resource must make an explicit copy or move decision, while externally owned memory should be documented.
  • Naked new and delete put the burden of matching every release on the programmer, so resource handles that release automatically are the preferred approach, and containers should provide an initializer-list constructor.

Related concepts

More concepts starting with R