Sobes.tech
Middle+

What should you keep in mind when an exception is thrown from a constructor?

sobes.tech AI

Answer from AI

When throwing an exception from a constructor, it is important to remember that the object may be partially initialized.

Resources acquired in the constructor before the exception is thrown (local variables, class members with automatic storage duration) are automatically released. Resources allocated dynamically (for example, via new) but not wrapped in smart pointers will not be automatically freed, leading to a memory leak.

Example of a memory leak:

#include <iostream>
#include <stdexcept>

class MyClass {
public:
    MyClass() : data1_(nullptr), data2_(nullptr) {
        data1_ = new int; // Memory allocated
        if (!data1_) throw std::runtime_error("Failed to allocate data1");

        data2_ = new int; // Memory allocated
        // Suppose something goes wrong here and an exception is thrown
        if (true /* error condition */) {
            // data1_ will not be deleted
            throw std::runtime_error("Error during initialization");
        }
    }

    ~MyClass() {
        delete data1_;
        delete data2_;
    }

private:
    int* data1_;
    int* data2_;
};

int main() {
    try {
        MyClass obj;
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    // Potential memory leak of data1_
    return 0;
}

Example using smart pointers to prevent leaks:

#include <iostream>
#include <stdexcept>
#include <memory> // For std::unique_ptr

class MyClass {
public:
    MyClass() : data1_(nullptr), data2_(nullptr) {
        data1_ = std::make_unique<int>(); // Memory managed by unique_ptr
        // Suppose something goes wrong here and an exception is thrown
        if (true /* error condition */) {
            // data1_ will be automatically deleted when out of scope
            throw std::runtime_error("Error during initialization");
        }
        data2_ = std::make_unique<int>();
    }

    // No need for a destructor (RAII)
    // ~MyClass() = default;

private:
    std::unique_ptr<int> data1_;
    std::unique_ptr<int> data2_;
};

int main() {
    try {
        MyClass obj;
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    // No memory leaks
    return 0;
}

It is crucial to follow the Resource Acquisition Is Initialization (RAII) principle, wrapping managed resources in objects whose destructors guarantee their release upon scope exit or object destruction. Smart pointers (std::unique_ptr, std::shared_ptr) are standard ways to implement RAII for dynamically allocated memory. Standard library containers also follow this principle.

Also, remember that the destructor of an object whose constructor threw an exception will not be called.

#include <iostream>
#include <stdexcept>

class MyClass {
public:
    MyClass() {
        std::cout << "Constructor called" << std::endl;
        throw std::runtime_error("Error in constructor");
    }

    ~MyClass() {
        // This destructor will not be called if the constructor throws an exception
        std::cout << "Destructor called" << std::endl;
    }
};

int main() {
    try {
        MyClass obj;
    } catch (const std::exception& e) {
        std::cerr << "Exception caught: " << e.what() << std::endl;
    }
    return 0;
}