﻿# С\+\+: освобождение ресурсов в деструкторах с использованием вспомогательных функций

В этой статье мы рассмотрим, как правильно разрушать объекты в ООП программе на языке C\+\+, не выполняя избыточных операций\. Этим мы завершим цикл публикаций, посвящённый обзору ошибок в игровом движке qdEngine\.

![1125_qdEngine_check_4_ru/image1.png](https://import.viva64.com/docx/blog/1125_qdEngine_check_4_ru/image1.png)

## Неудачная реализация очистки ресурсов в коде qdEngine

Предыдущие статьи о проверке игрового движка qdEngine:

1. [Топ 10 предупреждений PVS\-Studio](https://pvs-studio.ru/ru/blog/posts/cpp/1119/)\.
1. [Упрощение C\+\+ кода](https://pvs-studio.ru/ru/blog/posts/cpp/1121/)
1. [Дополнительная десятка багов](https://pvs-studio.ru/ru/blog/posts/cpp/1123/)

После этих публикаций у меня осталось ещё одно интересное предупреждение анализатора PVS\-Studio, которому я решил посвятить отдельную заметку\. Вот оно:

[V1053](https://pvs-studio.ru/ru/docs/warnings/v1053/) \[[CERT\-OOP50\-CPP](https://wiki.sei.cmu.edu/confluence/display/cplusplus/OOP50-CPP.+Do+not+invoke+virtual+functions+from+constructors+or+destructors)\] Calling the 'Finit' virtual function in the destructor may lead to unexpected result at runtime\. gr\_dispatcher\.cpp 54

В деструкторах можно вызывать виртуальные функции, и стандарт C\+\+ чётко описывает, как такой вызов работает\. К сожалению, такой код прямо\-таки притягивает ошибки, поэтому многие стандарты кодирования и анализаторы рекомендуют не делать таких вызовов\. В своё время я написал на эту тему статью "[Вызов виртуальных функций в конструкторах и деструкторах \(C\+\+\)](https://pvs-studio.ru/ru/blog/posts/cpp/0891/)"\. Если вы новичок в C\+\+ или подзабыли, как всё это работает, то предлагаю перед продолжением чтения заглянуть в неё\. Также предлагаю прочитать её тем, кто не понимает, о каких возможных ошибках идёт речь\.

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

<details>
   <summary>Код из проекта qdEngine\\\.</summary>

В деструкторе базового класса вызывается функция _Finit_\. Поскольку в этот момент класс\-наследник _DDraw\_grDispatcher_ уже разрушен, то его функция _Finit_ не будет вызвана\.

```cpp

class grDispatcher
{
  ....
  virtual ~grDispatcher();
  virtual bool Finit();
  ....
};

grDispatcher::~grDispatcher()
{
  Finit();
  if (dispatcher_ptr_ == this) dispatcher_ptr_ = 0;
}

bool grDispatcher::Finit()
{
#ifdef _GR_ENABLE_ZBUFFER
  free_zbuffer();
#endif

  flags &= ~GR_INITED;
  SizeX = SizeY = 0;
  wndPosX = wndPosY = 0;
  screenBuf = NULL;
  delete  yTable;
  yTable = NULL;

  return true;
}

class DDraw_grDispatcher : public grDispatcher
{
  ....
  ~DDraw_grDispatcher();
  bool Finit();
  ....
};

DDraw_grDispatcher::~DDraw_grDispatcher()
{
  if (ddobj_)
  {
    ddobj_ -> Release();
    ddobj_ = NULL;
  }

  video_modes_.clear();
}

bool DDraw_grDispatcher::Finit()
{
  grDispatcher::Finit();

  if (back_surface_)
  {
    while(
      back_surface_ -> GetBltStatus(DDGBS_ISBLTDONE) == DDERR_WASSTILLDRAWING);
    back_surface_ -> Unlock(&back_surface_obj_);
    ddobj_ -> SetCooperativeLevel((HWND)Get_hWnd(),DDSCL_NORMAL);
    if (fullscreen_ && ddobj_) ddobj_ -> RestoreDisplayMode();
  }

  if (prim_surface_)
  {
    prim_surface_ -> Release();
    prim_surface_ = NULL;
  }

  if (back_surface_)
  {
    back_surface_ -> Release();
    back_surface_ = NULL;
  }

  return true;
}
```


</details>
## Разбор ошибки на синтетическом коде

Теперь давайте разберёмся, в чём суть проблемы, используя синтетический код и сайт [Compiler Explorer](https://godbolt.org/), чтобы быстро изучать работу этого кода\.

Нужно создать иерархию классов, управляющую некими ресурсами\. При этом, используя полиморфизм, мы будем работать с объектами через указатель на базовый класс\.

Начнём с простейшего базового класса:

```cpp
#include <memory>
#include <iostream>

class Resource
{
public:
  void Create() {}
  void Destroy() {}
};

class A
{
  std::unique_ptr<Resource> m_a;

public:
  void InitA()
  {
    m_a = std::make_unique<Resource>();
    m_a->Create();
  }

  virtual ~A()
  {
    std::cout << "~A()" << std::endl;
    if (m_a != nullptr)
      m_a->Destroy();
  }
};

int main()
{
  std::unique_ptr<A> p = std::make_unique<A>();
  return 0;
}
```

Пока всё хорошо\. Запускаемый [online пример](https://godbolt.org/z/z8EdqWv3n) распечатывает:

```cpp
~A()
```

Далее выясняется, что время от времени нужно сбрасывать состояние класса, то есть освобождать ресурсы, не дожидаясь вызова деструктора при разрушении класса\. В этот момент допускается ошибка проектирования с созданием виртуальной функции для очистки класса\. Разработчик создаёт вот такой дополнительный виртуальный интерфейс класса:

```cpp
#include <memory>
#include <iostream>

class Resource
{
public:
  void Create() {}
  void Destroy() {}
};

class A
{
  std::unique_ptr<Resource> m_a;

public:
  void InitA()
  {
    m_a = std::make_unique<Resource>();
    m_a->Create();
  }

  virtual void Reset()
  {
    std::cout << "A::Reset()" << std::endl;
    if (m_a != nullptr)
    {
      m_a->Destroy();
      m_a.reset();
    }
  }

  virtual ~A()
  {
    std::cout << "~A()" << std::endl;
    Reset();
  }
};

int main()
{
  std::unique_ptr<A> p = std::make_unique<A>();
  return 0;
}
```

[Online пример печатает](https://godbolt.org/z/hTs4YGGqc):

```cpp
~A()
A::Reset()
```

Добавилась виртуальная функция _Reset_, освобождающая ресурсы\. Чтобы не дублировать код, деструктор теперь не сам освобождает ресурсы, а просто вызывает эту функцию\.

Пока кажется, что всё по\-прежнему хорошо, но давайте добавим класс\-наследник:

```cpp
#include <memory>
#include <iostream>

class Resource
{
public:
  void Create() {}
  void Destroy() {}
};

class A
{
  std::unique_ptr<Resource> m_a;

public:
  void InitA()
  {
    m_a = std::make_unique<Resource>();
    m_a->Create();
  }

  virtual void Reset()
  {
    std::cout << "A::Reset()" << std::endl;
    if (m_a != nullptr)
    {
      m_a->Destroy();
      m_a.reset();
    }
  }

  virtual ~A()
  {
    std::cout << "~A()" << std::endl;
    Reset();
  }
};

class B : public A
{
  std::unique_ptr<Resource> m_b;

public:
  void InitB()
  {
    m_b = std::make_unique<Resource>();
    m_b->Create();
  }

  void Reset()
  {
    std::cout << "B::Reset()" << std::endl;
    if (m_b != nullptr)
    {
      m_b->Destroy();
      m_b.reset();
    }
    A::Reset();
  }

  ~B()
  {
    std::cout << "~B()" << std::endl;
    Reset();
  }
};

int main()
{
  std::unique_ptr<A> p = std::make_unique<B>();
  p->Reset();
  std::cout << "------------" << std::endl;
  p->InitA();
  return 0;
}
```

[Online пример печатает](https://godbolt.org/z/rPzbdrfo1):

```cpp
B::Reset()
A::Reset()
------------
~B()
B::Reset()
A::Reset()
~A()
A::Reset()
```

Если из внешнего кода явно вызываем функцию _Reset_, то всё отрабатывает хорошо\. Вызывается _B::Reset\(\)_, которая затем вызывает одноимённую функцию из базового класса\.

Проблема с деструктором\. Каждый деструктор вызывает функцию _Reset_\. Возникает избыточность, так как функция _Reset_ вызывает свой вариант из базового класса\.

Если мы продолжим наследоваться дальше, то всё больше будем усугублять проблему\. Всё больше и больше лишних вызовов функций\.

[Вывод кода](https://godbolt.org/z/dnGKEr95r), где добавлен ещё один класс:

```cpp
C::Reset()
B::Reset()
A::Reset()
------------
~C()
C::Reset()
B::Reset()
A::Reset()
~B()
B::Reset()
A::Reset()
~A()
A::Reset()
```

Ошибка проектирования класса очевидна\. Впрочем, это сейчас она очевидна нам\. В реальном проекте такие ошибки вполне себе могут жить, оставаясь незамеченными: код ведь работает, и функции сброса состояния класса справляются со своей задачей\. Проблема в избыточных действиях и неэффективности\. 

Заметив описанную ошибку и пытаясь её исправить, программист рискует допустить две другие типовые ошибки\.

**Первый вариант\.** Объявить функции _Reset_ невиртуальными и не вызывать в них базовые варианты \(_x::Reset_\)\. Тогда каждый деструктор будет вызывать только функцию _Reset_ из своего класса и освобождать только свои ресурсы\. Это действительно уберёт избыточность при работе деструкторов\. Однако сломается очистка состояния объекта при вызове _Reset_ извне\. [Сломанный код распечатает](https://godbolt.org/z/TM9eqo4Pz):

```cpp
A::Reset()   // Сломали очистку ресурсов из вне
------------
~C()
C::Reset()
~B()
B::Reset()
~A()
A::Reset()
```

**Второй вариант\.** Вызвать виртуальную функцию _Reset_ однократно из деструктора базового класса\. Это не будет работать, так как согласно правилам C\+\+ будет вызвана функция _Reset_, реализованная в базовом классе, а не в наследниках\. Это логично, так как к моменту вызова деструктора _\~A\(\)_ все наследники разрушены, и вызывать функции из них нельзя\. [Сломанный код распечатает](https://godbolt.org/z/6cs9aj4aG):

```cpp
C::Reset()
B::Reset()
A::Reset()
------------
~C()
~B()
~A()
A::Reset()  // Освобождаем ресурсы только в базовом классе
```

Именно этот тип ошибки и был найден в проекте qdEngine благодаря PVS\-Studio\. При желании теперь можете вернуться к началу статьи и посмотреть соответствующий код из движка\.

## Исправленный синтетический код

Как же правильно реализовать классы, чтобы избежать множественных избыточных вызовов?

Нужно отделить очистку ресурсов в классах от интерфейса для их очистки извне\. Следует создать невиртуальные функции, отвечающие только за очистку данных в классах, где они объявлены\. Назовём их _ResetImpl_ и сделаем приватными, так как они не предназначены для внешнего использования\.

Деструкторы просто будут делегировать свою работу функциям _ResetImpl_\.

Функция _Reset_ останется публичной и виртуальной\. Она будет очищать данные всех классов, используя всё те же вспомогательные функции _ResetImpl_\.

Соберём всё вместе и напишем корректный код:

```cpp
#include <memory>
#include <iostream>

class Resource
{
public:
  void Create() {}
  void Destroy() {}
};

class A
{
  std::unique_ptr<Resource> m_a;

  void ResetImpl()
  {
    std::cout << "A::ResetImpl()" << std::endl;
    if (m_a != nullptr)
    {
      m_a->Destroy();
      m_a.reset();
    }
  }

public:
  void InitA()
  {
    m_a = std::make_unique<Resource>();
    m_a->Create();
  }

  virtual void Reset()
  {
    std::cout << "A::Reset()" << std::endl;
    ResetImpl();
  }

  virtual ~A()
  {
    std::cout << "~A()" << std::endl;
    ResetImpl();
  }
};

class B : public A
{
  std::unique_ptr<Resource> m_b;

  void ResetImpl()
  {
    std::cout << "B::ResetImpl()" << std::endl;
    if (m_b != nullptr)
    {
      m_b->Destroy();
      m_b.reset();
    }
  }

public:
  void InitB()
  {
    m_b = std::make_unique<Resource>();
    m_b->Create();
  }

  virtual void Reset()
  {
    std::cout << "B::Reset()" << std::endl;
    ResetImpl();
    A::Reset();
  }

  virtual ~B()
  {
    std::cout << "~B()" << std::endl;
    ResetImpl();
  }
};

class C : public B
{
  std::unique_ptr<Resource> m_c;

  void ResetImpl()
  {
    std::cout << "C::ResetImpl()" << std::endl;
    if (m_c != nullptr)
    {
      m_c->Destroy();
      m_c.reset();
    }
  }

public:
  void InitC()
  {
    m_c = std::make_unique<Resource>();
    m_c->Create();
  }

  virtual void Reset()
  {
    std::cout << "C::Reset()" << std::endl;
    ResetImpl();
    B::Reset();
  }

  virtual ~C()
  {
    std::cout << "~C()" << std::endl;
    ResetImpl();
  }
};

int main()
{
  std::unique_ptr<A> p = std::make_unique<C>();
  p->Reset();
  std::cout << "------------" << std::endl;
  return 0;
}
```

[Online пример печатает](https://godbolt.org/z/G3ohvE6EY):

```cpp
C::Reset()
C::ResetImpl()
B::Reset()
B::ResetImpl()
A::Reset()
A::ResetImpl()
------------
~C()
C::ResetImpl()
~B()
B::ResetImpl()
~A()
A::ResetImpl()
```

Не могу сказать, что синтетический код выглядит красиво\. В нём рябит от букв A, B, C, и очень легко опечататься\. Простим это синтетическим примерам\. Главное, что код работает, и мы избавились от избыточных операций\.

## Примечание

Схожая проблема может происходить с конструкторами, когда используют функции инициализации, которые вызываются в базовых классах по несколько раз\. Пример такой ситуации и её исправления представлен в видео про code\-review проекта Lyra \(раздел "Работаем с Replit"\)\.

<https://www.youtube.com/watch?v=ZDR8sP9ei2k&t=2101s>

## Заключение

Сам по себе вызов виртуальной функции в деструкторе не обязательно является ошибкой\. Однако это может являться признаком плохого дизайна классов\. Как раз это мы и наблюдали в случае проекта _qdEngine_\.

Анализатор PVS\-Studio выдаёт предупреждение [V1053](https://pvs-studio.ru/ru/docs/warnings/v1053/) в том случае, если виртуальная функция вызывается в конструкторе или деструкторе\. Это повод ещё раз проверить такой код, и, возможно, исправить его или провести рефакторинг\.