Об опасности исключений

Ноябрь 12th, 2009 по SadKo Оставить ответ »

Об опасностях, таящихся в мороженом корме коде, генерирующем исключения.
Исключения в C++ достаточно опасны, именно поэтому я стараюсь их не использовать.
Давайте разберёмся вот с этим кодом:

#include <stdio.h>
 
class x
{
    public:
        x() { printf("x()\n"); }
        ~x() { printf("~x()\n"); }
};
 
class e
{
    public:
        e() { printf("e()\n"); }
        e(const e& src) { printf("e(src)\n"); }
        ~e() { printf("~e()\n"); }
};
 
void g(void)
{
    throw e();
}
 
void f(void)
{
    x* ptr = new x();
    g();
    delete ptr;
}
 
int main(void)
{
    try
    {
        f();
    }
    catch (e)
    {
        printf("Caught exception e\n");
    }
}

Казалось бы, достаточно простой код, но он таит в себе Memory leak: объект класса x создаётся, но никогда не уничтожается. Это очень вырожденный пример, но он передаёт очень важную суть: если вы не ожидаете от функции g(), что она будет генерировать исключение, и пишете код, то когда функция g() начинает генерировать исключения, код становится работающим некорректно, в чём вы и можете убедиться, запустив программу:

x()
e()
e(src)
Caught exception e
~e()
~e()

Обойти это можно, сделав класс-обвязку (а-ля smart ptr):

template <class ptr>
    class p
    {
        private:
            ptr *m_ptr;
 
        public:
            p(): m_ptr(0) {}
            p(ptr *pp): m_ptr(pp) {}
            ~p() { if (m_ptr!=0) delete m_ptr; }
 
            ptr *drop()
            {
                ptr *res = m_ptr;
                if(m_ptr!=0)
                {
                    delete m_ptr;
                    m_ptr = 0;
                }
                return res;
            }
 
            ptr &operator *() { return *m_ptr; }
            ptr &operator ->() { return *m_ptr; }
 
            bool validate() { return m_ptr != 0; }
            operator bool() { return m_ptr != 0; }
    };

И переписав функцию f():

void f(void)
{
    p<x> ptr(new x());
    if (ptr)
        printf("OK, validated\n");
    g();
}

Получаем вполне нормальное поведение:

x()
OK, validated
e()
~x()
e(src)
Caught exception e
~e()
~e()

Но… Если функция g() была использована во многих местах, это же получается тотальный рефакторинг кода! И не надо меня убеждать в том, что изначально надо было пользоваться штуками типа smart ptr!

Решить проблему можно, сделав сеппуку для функции g() и разбив её на две функции: враппер и, собственно, саму реализацию, то есть:

void _g(void)
{
    throw e();
}
 
void g(void)
{
    try
    {
        _g();
    }
    catch (e)
    {
        printf("Caught exception e\n");
    }
}
 
void f(void)
{
    x *ptr = new x();
    g();
    delete ptr;
}
 
int main(void)
{
    f();
}

То есть, мы избежим тотального рефакторинга, но при этом сможем пользоваться новой функцией _g(), которая будет кидать исключения. С другой стороны, это не всегда корректно. Представим себе ситуацию, что генерация исключения e() требует обязательного освобождения какого-либо ресурса, о котором функция _g() не знает (и ввиду её специфичной реализации не должна знать). Тогда если код, который будет отлавливать исключение e(), будет неявно пользоваться и g(), и _g(), то он рискует не освободить ресурс, когда это надо. Характерный пример:

class x
{
    public:
        x() { printf("x()\n"); }
        ~x() { printf("~x()\n"); }
        void alarm() { printf("alarm!\n"); }
};
 
void _g(void)
{
    throw e();
}
 
void g(void)
{
    try
    {
        _g();
    }
    catch (e)
    {
        printf("Caught exception e\n");
    }
}
 
void z(bool r)
{
    if (r) g();
    else _g();
}
 
void f(bool r)
{
    x *ptr = new x();
    try
    {
        z(r);
    }
    catch (e)
    {
        ptr->alarm();
    }
 
    delete ptr;
}
 
int main(void)
{
    f(true);
    f(false);
}

В результате выполнения получаем:

x()
e()
e(src)
Caught exception e
~e()
~e()
~x()
x()
e()
e(src)
alarm!
~e()
~e()
~x()

То есть, «Caught exception e» для нас — это неожиданная ситуация, так как мы ожидаем, что если _g() не отработала, то мы должны сами словить исключение и получить «alarm!».

Таким образом, исключения в C++ таят достаточно большую опасность, если их использовать неаккуратно. Попытка обойти подобную ситуацию есть в java — это специальная директива throws у метода, которая вынуждает разработчика либо ловить исключение, либо передавать его дальше, что, хоть, и не спасает от генерации Runtime Exception, но помогает сразу найти места, где вызывается метод, который генерирует исключение (потому что иначе просто ничего не скомпилится).

В общем, на этом пока мысли останавливаются.

Реклама

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

  1. Indy:

    > Таким образом, исключения в C++ таят достаточно большую опасность, если их использовать неаккуратно.
    Ну так не юзайте гуан, выберайте среду, функционал которой достаточен и должен быть избыточным для реализации. Исключение с точки зрения треда с кпл 3 это всеголишь сохранение контекста и передача управления на одну из фиксированных точек, которые по своей сути являются ядерными калбэками. Нет абсолютно никаких проблем с обработкой сепшенов(за исключением разрушения стека). Кстате кресты относятся к быдлокодерским языкам ;)

  2. А кто сказал, что C++ — гуано :) ?
    Просто нужно представлять, какие элементы какую опасность таят.

  3. Indy:

    > А кто сказал, что C++ – гуано ?
    На лукоморье сказано :D
    > Просто нужно представлять, какие элементы какую опасность таят.
    Надстройку сделайте собственную, зачем тогда сех.

Добавить комментарий

Blue Captcha Image
Refresh

*