C++ Programming Tips: Resource Leaks Caused by Constructor Exceptions

If an exception occurs in the constructor of a class, the constructed object will be incomplete, indicating a construction failure. This incomplete object can lead to resource leaks.Assuming we use a “People” class to represent a human, this class should include name, age, facial data, and fingerprint data. We typically write it like this:

// Face class class Face{    ...};// Fingerprint classclass Fingerprint{    ...};// Human classclass People{public:    People(const std::string& Name,           const int Age,           const std::string& FaceData,           const std::string& FingerprintData);    ~People();private:    std::string Name;    int Age;    Face* theFace;    Fingerprint* theFingerprint;};// ConstructorPeople::People(const std::string& Name,       const int Age,       const std::string& FaceData,       const std::string& FingerprintData):       Name(Name),Age(Age),theFace(0),theFingerprint(0){    if(FaceData != ""){         theFace = new Face(FaceData);    }    if(FingerprintData != ""){         theFingerprint= new Fingerprint(FingerprintData);    }}// DestructorPeople::~People(){    delete theFace;    delete theFingerprint;}

In the code above, pay special attention to the definition of the constructor, where the face pointer and fingerprint pointer are first initialized to nullptr, and then reassigned internally. Finally, in the destructor, these two pointers are deleted. Since C++ allows deleting null pointers, there is no need to check if they are null again.Now, the problem arises. Suppose an exception is thrown when creating the fingerprint object in the constructor; this exception will be thrown by the constructor, resulting in a failure to construct and leaving an incomplete People object.Look at this incomplete People object, which has initialized Name, Age, and a theFace pointer that already points to a heap-allocated face object.Name and Age will be destroyed with this incomplete object, but the theFace pointer pointing to the heap-allocated face object will not. Because the destruction of an incomplete object does not call the destructor, a resource leak occurs.There are two methods to avoid this problem,namely try-catch and smart pointers. I will write both here. However, using smart pointers is generally recommended, so we can skip the try-catch method.1. Try-Catch Method1. Use try-catch in the constructor

// ConstructorPeople::People(const std::string& Name,       const int Age,       const std::string& FaceData,       const std::string& FingerprintData):       Name(Name),Age(Age),theFace(0),theFingerprint(0){    if(FaceData != ""){         theFace = new Face(FaceData);    }    if(FingerprintData != ""){        try{                  // New code            theFingerprint= new Fingerprint(FingerprintData);        }catch(...){            delete theFace;            throw; // Throw exception        }    }}

First, the new process for theFace does not need try-catch because it is the first assignment; if there is a problem, there are no heap objects to release in the incomplete People object.Afterward, the new process for theFingerprint needs to be wrapped in try-catch. When an exception occurs, theFace is released in the catch block, and the exception is thrown.2. Avoid code duplication in catch and destructor

// ConstructorPeople::People(const std::string& Name,       const int Age,       const std::string& FaceData,       const std::string& FingerprintData):       Name(Name),Age(Age),theFace(0),theFingerprint(0){    if(FaceData != ""){         theFace = new Face(FaceData);    }    if(FingerprintData != ""){        try{                  // New code            theFingerprint= new Fingerprint(FingerprintData);        }catch(...){            Clean();            throw; // Throw exception        }    }}// DestructorPeople::~People(){    Clean();}// Shared cleanup functionvoid People::Clean(){    delete theFace;    delete theFingerprint;}

The code in catch and destructor is duplicated. To avoid this situation, we can simply place the cleanup process in a shared function.There is no need to worry about delete theFingerprint causing issues in catch because we have already initialized it to nullptr; deleting a null pointer is not a problem.3. How to handle constant pointers

// If theFace and theFingerprint are constant pointersFace* const theFace;Fingerprint* const theFingerprint;// ConstructorPeople::People(const std::string& Name,       const int Age,       const std::string& FaceData,       const std::string& FingerprintData):       Name(Name),Age(Age),       theFace(FaceData == ""?0:new Face(FaceData)),       theFingerprint(FingerprintData == ""?0:new Fingerprint(FingerprintData)){}

When theFace and theFingerprint are constant pointers, we cannot reassign them in the constructor; they can only be initialized through the initializer list.However, we cannot place try-catch statements in the initializer list, so we need to use functions to indirectly utilize them:

// Function to initialize theFace// Since it is the first initialization, no need for try-catchFace* People::InitTheFace(const std::string& FaceData){    if(FaceData != "") return new Face(FaceData);    else return 0;}// Function to initialize theFingerprint// Needs try-catchFingerprint* People::InitTheFingerprint(const std::string& FingerprintData){    if(FingerprintData != ""){        try{            return new Fingerprint(FingerprintData);        }catch(...){            delete theFace;            throw;        }    }    else return 0;}// ConstructorPeople::People(const std::string& Name,       const int Age,       const std::string& FaceData,       const std::string& FingerprintData):       Name(Name),Age(Age),       theFace(InitTheFace(FaceData)),       theFingerprint(InitTheFingerprint(FingerprintData)){}

At this point, our method for handling constructor exceptions that lead to resource leaks is complete.However, many friends may feel that this method is too complicated and that it disperses the tasks of the constructor into other functions, making the structure unclear. If you have these concerns, the following smart pointer method will be very appealing to you.2. Smart Pointer Method

// Non-pointer constant formauto_ptr<Face> theFace;auto_ptr<Fingerprint> theFingerprint;// Pointer constant formconst auto_ptr<Face> theFace;const auto_ptr<Fingerprint> theFingerprint;// Both non-pointer constant and pointer constant are handled the same way// ConstructorPeople::People(const std::string&amp; Name,       const int Age,       const std::string&amp; FaceData,       const std::string&amp; FingerprintData):       Name(Name),Age(Age),       theFace(FaceData == ""?0:new Face(FaceData)),       theFingerprint(FingerprintData == ""?0:new Fingerprint(FingerprintData)){}

Very simple, regardless of whether the pointer is a constant pointer, the handling process is the same; just use smart pointer objects instead of the original pointers. Also, there is no need to delete in the destructor.Let’s analyze why this is the case. When an exception occurs, the destruction of the incomplete People object will trigger the destruction of theFace, and the destruction of the smart pointer object will call its destructor, which deletes the heap object.If the construction is successful, the destruction of the People object will also trigger the destruction of the smart pointer, which will similarly delete the heap object. Therefore, the destructor of People does not need to delete again.Thus, the problem of resource leaks caused by constructor exceptions has been resolved.

Leave a Comment