← SEARCHABLE ARCHIVE
C / C++ · 2026-08-09

C++ from Basics to Mastery — Part 15

Manage resources safely even when exceptions occur

INTERACTIVE READING MODE →

The articles written on this blog were created with the goal of teaching. Also, the information in the article may be incorrect, so please be careful.

If a problem occurs in the intermediate code after reading the file and allocating memory, you must clean up the resources already obtained. If you manually write cleanup code for every return and exception path, it's easy to miss it.

If an exception occurs

An exception is a way to convey a failure that cannot be handled at the current location to the calling party.

Item LoadItem(int id)
{
    if (id <= 0)
    {
        throw std::invalid_argument("id must be positive");
    }

    return ReadItemFromFile(id);
}

try
{
    Item item = LoadItem(inputId);
}
catch (const std::exception& error)
{
    std::cerr << error.what() << '\n';
}

When throw is executed, the code below the current line is skipped and a matching catch is found. In the process, the destructor of the already created local object is called.

Not all failures require exceptions. Expectable results, such as item search failure, may be more obvious as optional or bool.

Organizing with RAII

By tying the lifespan of a resource to the lifespan of an object, there is no need to separate normal return and exception paths.

std::string ReadConfig(const std::filesystem::path& path)
{
    std::ifstream file(path);
    if (!file)
    {
        throw std::runtime_error("config open failed");
    }

    std::string text(
        std::istreambuf_iterator<char>{file},
        std::istreambuf_iterator<char>{}
    );

    return text;
}

Even if an exception occurs because string creation fails in the middle, ifstream's destructor closes the file. uniqueptr for dynamic objects, uniqueptr for locksstd::lock_guard, container memory plays the same role as vector.

void UpdateInventory()
{
    std::lock_guard<std::mutex> lock(inventoryMutex);
    RebuildInventory();
}

Even if an exception occurs in RebuildInventory, the lock destructor releases the mutex. It reduces mistakes that can occur when directly pairing lock and unlock.

exception safety

You need to think about what state your function guarantees when an exception occurs.

  • Basic guarantee: There are no resource leaks and objects remain usable.
  • Strong guarantee: If the operation fails, the state before the call is maintained.
  • Guaranteed no exceptions: the function does not throw any exceptions.

It is easy to create strong guarantees by completing the new state in a temporary object and replacing it at the end.

void Inventory::ReplaceItems(std::vector<Item> newItems)
{
    items.swap(newItems);
}

If there is a failure while creating call arguments, existing items will not be changed. Inside the function, the new list completed by swap replaces the existing list.

noexcept

For functions that do not throw exceptions,noexceptcan be displayed.

Player(Player&& other) noexcept = default;
Player& operator=(Player&& other) noexcept = default;

A vector determines whether it can be safely moved when reallocating elements. If the move constructor is noexcept, it is likely to choose move instead of copy.

However, an exception may occur internally, but if you add noexcept, the moment the exception goes out,std::terminateis called. It is not a keyword added blindly for performance, but a promise not to actually throw an exception.

The key to exception safety is not writing a lot of catches. Resources are handled by RAII objects, and tasks are organized in an order that does not destroy the existing state even if it fails.