Неизвестный C++: exceptions

Во многих книгах пишут, что нужно избегать exception-ов в конструкторах и деструкторах. Exception в конструкторе нам как бы «подсказывает», что объект не был сконструирован до конца и следовательно, нет смысла вызывать деструктор для такого объекта. Такое поведение чревато утечками ресурсов, поскольку как правило ресурсы освобождаются в деструкторе.

Пример:

#include <iostream>
#include <memory>

using namespace std;

class B
{
public:
   B()
   {
      cout << __FUNCTION__ << endl;
   }

   ~B()
   {
      cout << __FUNCTION__ << endl;
   }
};

class A
{
   B* PtrToB;
   auto_ptr<B> autoB;
public:
   A()
   {
      PtrToB = new B;
      autoB.reset(new B);

      cout << __FUNCTION__ << endl;
      throw 1;
   }

   ~A()
   {
      delete PtrToB;
      cout << __FUNCTION__ << endl;
   }
};

void main()
{
   cout << __FUNCTION__ << endl;

   {
      A a;
   }
}

Результат:
main
B::B
B::B
A::A
!Exception!

Как видно из этого примера, даже деструктор автоматически созданного объекта не вызывался.

(Note: В действительности если бы это был не unhandled exception, то деструктор для B вызывался. Смотри обсуждение в комментариях.)

Добавляем try/catch в конструктор:

class B
{
public:
   B()
   {
      cout << __FUNCTION__ << endl;
   }

   ~B()
   {
      cout << __FUNCTION__ << endl;
   }
};

class A
{
   B* PtrToB;
   auto_ptr<B> autoB;
public:
   A()
   {
      try
      {
         PtrToB = new B;
         autoB.reset(new B);

         cout << __FUNCTION__ << endl;
         throw 1;
      }
      catch (...)
      {
         cout << "I got it" << endl;
      }

      cout << "End of function" << endl;
   }

   ~A()
   {
      delete PtrToB;
      cout << __FUNCTION__ << endl;
   }
};

void main()
{
   cout << __FUNCTION__ << endl;

   {
      A a;
   }
}

И результат:

main
B::B
B::B
A::A
I got it
End of function
B::~B
A::~A
B::~B

Exception поймали и работает как часы! Но, что делать, если exception происходит в конструкторе или деструкторе наследуемого объекта:

...
class C
{
public:
   C()
   {
      cout << __FUNCTION__ << endl;
      throw 1;
   }

   ~C()
   {
      cout << __FUNCTION__ << endl;
   }
};

class A: public C
{
   B* PtrToB;
   auto_ptr<B> autoB;
public:
   A()
...

Результат:
main
C::C
!Exception!

Попробуем так:

class A: public C
{
   B* PtrToB;
   auto_ptr<B> autoB;
public:
   A()
   try
   {
      PtrToB = new B;
      autoB.reset(new B);

      cout << __FUNCTION__ << endl;
      throw 1;
   }
   catch (...)
   {
      cout << "I got it" << endl;
      /* here is hidden throw */
   }
...

Результат:
main
C::C
I got it
!Exception!

Вроде бы поймали, а вроде бы и нет ;). Exception поймали, но почему-то ничего дальше не произошло. Открываем disassembler и видим:

automatic throw

Сразу в конце блока catch компилятор вставил throw. Идея в том, что, если exception произошёл в констукторе базового класса, то это уже «фатально» и мы можем только освободить свои ресурсы, не более. Что делать, если всё таки хочется заглушить exception? Для этого есть «легальный» способ — добавить return до окончания блока catch:

class A: public C
{
   B* PtrToB;
   auto_ptr<B> autoB;
public:
   A()
   try
   {
      PtrToB = new B;
      autoB.reset(new B);

      cout << __FUNCTION__ << endl;
      throw 1;
   }
   catch (...)
   {
      cout << "I got it" << endl;
      return;
      /* here is hidden throw */
   }
...

Результат:
main
C::C
I got it
B::~B
!Exception!

Почти добились цели, теперь конструктор не падает с exception-ом. Но PtrToB не проинициализирован и как результат программа падает при вызове в A::~A на освобождении памяти delete PtrToB. Переписываем ещё раз код в надежде, что теперь всё будет работать:

class A: public C
{
   B* PtrToB;
   auto_ptr<B> autoB;

   void Init() throw()
   {
      PtrToB = new B;
      cout << "before autoB.reset() " << endl;

      autoB.reset(new B);

      cout << __FUNCTION__ << endl;
   }
public:
   A()
   try
   {
      Init();
      cout << __FUNCTION__ << endl;
   }
   catch (...)
   {
      Init();
      cout << "I got it" << endl;
      return;
      /* here is hidden throw */
   }
...

Результат:
main
C::C
B::B
before autoB.reset()
B::B
B::~B
!exception!

Проблема в том, что компилятор не вызвал конструктор объекта autoB и в нём хранится мусор. Как результат во время вызова auto_ptr<B>::reset внутренний указатель проверяется на NULL, поскольку указатель является !NULL, то метод reset пытается удалить не существующий объект. Можно найти много вариантов как обойти эту проблему, но я оставлю это за рамками изложения данного материала.

Что делать если у вас exception происходит в списке инициализации конструктора? Необходимо написать try сразу после объявления конструктора, но до списка инициализации, вот таким образом:

class B
{
public:
   B()
   {
      throw 1;
      cout << __FUNCTION__ << endl;
   }

   ~B()
   {
      cout << __FUNCTION__ << endl;
   }
};

class C
{
public:
   C()
   {
      cout << __FUNCTION__ << endl;
   }

   ~C()
   {
      cout << __FUNCTION__ << endl;
   }
};

class A: public C
{
   B* PtrToB;
public:
   A() try: C(), PtrToB(new B)
   {
      cout << __FUNCTION__ << endl;
   }
   catch (...)
   {
      //Hack: if an exception happens, PtrToB will have a garbage
      PtrToB = 0;
      cout << "I got it" << endl;
      return;
      /* here is hidden throw */
   }

   ~A()
   {
      delete PtrToB;
      cout << __FUNCTION__ << endl;
   }
};

Результат:
main
C::C
C::~C
I got it
A::~A
C::~C

Наконец-то программа доработала до конца без exception-ов. На этой оптимистической ноте хотел бы закончить.

P.S. Хотя в коде остался memory leak. Есть варианты его исправления, но поиск решения оставлю читателю.

8 комментариев

  1. Интересные выдержки из стандарта на тему:

    [15.3 (Handling an exception).14] «If a return statement appears in a handler of the function-try-block of a constructor, the program is ill-formed.»

    [15.3 (Handling an exception).15] «The currently handled exception is rethrown if control reaches the end of a handler of the function-try-block
    of a constructor or destructor. Otherwise, a function returns when control reaches the end of a handler for
    the function-try-block (6.6.3). Flowing off the end of a function-try-block is equivalent to a return with no
    value; this results in undefined behavior in a value-returning function (6.6.3).»

  2. Простое следование стратегии RAII делает ненужным перехват исключений в конструкторах и все последующие хитроумные приседания.

    • Утверждение неверно. В первом примере специально был добавлен auto_ptr< B > autoB, чтобы показать, что RAII не работает для таких use-case-ов.

      «Результат:
      main
      B::B
      B::B
      A::A
      !Exception!»

      Т.е. не был вызван деструктор класса B, хотя autoB был успешно проинициализирован.

      • В С++ есть простое правило: для любого (полностью) сконструированного объекта вызывается деструктор. Поэтому RAII всегда работает.

        Первый пример некорректен. В main следует добавить блок try/catch. Тогда вывод будет следующий:
        main
        B::B
        B::B
        A::A
        B::~B
        Потому что вызов деструкторов при исключении происходит при раскрутке стека. А если блока try/catch нет, то работа приложения фактически завершается в момент генерации исключения и никакой раскрутки не выполняется.

        • Согласен отчасти, такой код тоже выход:

          void main()
          {
             try  
             { 
                /* some code */ 
             }
             catch (...) 
             { 
                throw; 
             }
          }
          

          Однако, в случае unhandled exception RAII не работает => работает не всегда. (Добавил комментарий в текст поста по этому поводу.)

          В действительности, если код является библиотекой, то нет гарантий как он будет вызываться. А при описанных выше «хитроумных приседаниях» можно по крайней мере вывести лог если произойдёт (unhandled) exception в конструкторе базового класса, например, если этот конструктор выкидывает exception по спецификации (throw (…)).

          P.S. Сергей, спасибо за комментарии.

          • На практике RAII работает всегда. Если в программе появилось необработанное исключение, то вопрос об утечке ресурсов теряет всякий смысл. Операционная система подчистит всё что следует.

            Для трассировки ошибок что-то из описанного возможно и пригодится, но это другой разговор.

          • >>Операционная система подчистит всё что следует.
            [AK] Зависит от OS. Например, windows делает почти всё: «When the system is terminating a process, it does not terminate any child processes that the process has created.»Как этого можно достигнуть другая тема. Мысль в том, что use-case-ы использования function-try-block в конструкторах существуют. Конечно, есть альтернативы, но не для всех use-case-ов. И конечно в тексте поста есть некая «искусственность» примеров, но я пиши текст специально, чтобы примеры были наиболее наглядны и понятны, чтобы читать их было не сложно.

  3. Ещё один интересный пример как в С++ не явно вызывается delete для памяти выделяемой для объекта через new, если в конструкторе происходит exception.

    #include <iostream>
    #include <memory>
     
    using namespace std;
     
    void* operator new(std::size_t size)
    {
       void* pointer = malloc(size);
       cout << "new pointer:" << pointer << " size: " << size << endl;
       return pointer;
    }
    
    void operator delete(void* pointer) 
    {
       cout << "free ptr:" << pointer << endl;
       free(pointer);
    }
    
    class B
    {
    public:
       B()
       {
          cout << __FUNCTION__ << endl;
          throw 1;
       }
     
       ~B()
       {
          cout << __FUNCTION__ << endl;
       }
    };
      
    void main()
    {
       cout << __FUNCTION__ << endl;
     
       try
       {
          auto_ptr< B > b(new B());
       }
       catch(...)
       {
         cout << "Exception" << endl;
       }
    }
    

    Результат:
    main
    new pointer:00424990 size: 1
    B::B
    free ptr:00424990
    Exception

Добавить комментарий для Alexey Kodubets Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *

@ 2010- Кодубец Алексей Александрович
При копировании материалов обратная ссылка обязательна.