Как обнаружить раскручивание стека в деструкторе на С++?

Опубликовано: 18 Февраля, 2023

Что такое разматывание стека?

Stack unwinding is the process of executing destructors for all local objects when an exception propagates out of a function. It happens when an exception is thrown and not caught within the same function. When this occurs, the destructors for all objects with automatic storage duration declared in that function are called in reverse order of their declaration before the control is transferred to a handler (if any) or returned to the caller.

Раскручивание стека обычно прозрачно для программиста и происходит автоматически. Разматывание стека обычно связано с обработкой исключений. Когда в C++ возникает исключение, в стеке вызовов функций линейно ищется обработчик исключений, и все записи перед функцией с обработчиком исключений удаляются. Если исключение не обрабатывается в том же коде, требуется раскручивание стека (там, где оно выбрасывается). Раскручивание стека — это, по сути, процесс вызова деструкторов для всех автоматизированных объектов, созданных во время выполнения (всякий раз, когда возникает исключение).

Различные подходы к разматыванию стека:

Существуют разные подходы к теме раскручивания стека в деструкторе.

  • Один из способов — посмотреть на это с точки зрения того, что происходит, когда генерируется исключение.
  • Другой способ — посмотреть на это с точки зрения того, как вызывается деструктор, когда объект выходит за пределы области видимости.

Взгляд на раскрутку стека с точки зрения выбрасываемого исключения может помочь нам понять, почему важно иметь деструктор, который может очищаться после себя.

Когда возникает исключение:

Выполнение программы переходит к ближайшему блоку catch. Но прежде чем это произойдет, все объекты, созданные в блоке try, будут уничтожены. Сюда входят любые локальные объекты, а также любые объекты, созданные путем динамического выделения памяти. Если деструкторы для этих объектов не очищаются должным образом, это может привести к утечке памяти или другим проблемам.

С точки зрения того, как называется деструктор:

Это может помочь понять важность деструктора. Деструктор вызывается всякий раз, когда объект выходит за пределы области видимости. Когда объект выходит из области видимости, вызывается его деструктор, и все ресурсы, которые он использовал, освобождаются. Если деструктор не выполняет очистку должным образом, это может привести к утечке ресурсов или другим проблемам.

Как обнаружить раскручивание стека?

В деструкторе раскрутку стека можно обнаружить, ища признаки активности очистки, такие как следующие:

  • Вызов функций, освобождающих ресурсы (например, закрытие файлов или освобождение памяти)
  • Регистрация сообщений
  • Установка флагов

Если какое-либо из этих действий наблюдается в деструкторе, вполне вероятно, что происходит раскручивание стека.

Использование std::uncaught_exception()

Вы можете использовать функцию std::uncaught_exception() , которая возвращает true, если исключение обрабатывается в данный момент (т. е. сгенерировано, но еще не перехвачено). Таким образом, в вашем деструкторе вы должны проверить, возвращает ли std::uncaught_exception() значение true или false, и предпринять соответствующие действия.

Пример:

C++




#include <bits/stdc++.h>
using namespace std;
  
class MyClass {
public:
    MyClass()
    {
        // allocate some resource 
    }
  
    ~MyClass() 
    {
        if (!std::uncaught_exception()) {
            // release resource
        }
                
    };
};

Переопределение функции std::terminate()

Чтобы определить, когда происходит раскручивание стека, вы можете переопределить функцию std::terminate() . Среда выполнения вызывает эту функцию, когда не может найти подходящий обработчик исключений. Переопределяя std::terminate() , вы можете установить точку останова или записать сообщение, чтобы облегчить отладку вашей программы.

Вот пример переопределения std::terminate():

C++




void my_terminate()
{
    // Set breakpoint here or log message
    std::cerr << "Stack unwinding detected!" << std::endl;
  
    // Call original terminate function
    std::terminate();
}
  
int main()
{
    // Install our custom terminate handler
    std::set_terminate(my_terminate);
  
    try {
        // Code that might throw an exception goes here...
    }
    catch (...) {
        // Exception handlers go here...
    }
    return 0;
}

Установив флаг в конструкторе:

Установите флаг в конструкторе и проверьте этот флаг в деструкторе. Если флаг установлен, то вы знаете, что деструктор был вызван из-за исключения. Вот пример:

C++




class MyClass {
public:
    MyClass()
        : m_isUnwinding(false)
    {
        // Constructor
    }
  
    ~MyClass()
    {
        if (m_isUnwinding) {
            // The stack is unwinding because of an
            // exception
        }
        else {
            // No exception is being handled
        }
    };
};

Статьи по Теме:

  • Введение в память стека?
  • Обработка исключений в C++
  • Деструкторы в С++