Показаны сообщения с ярлыком exceptions. Показать все сообщения
Показаны сообщения с ярлыком exceptions. Показать все сообщения

вторник, 1 июля 2008 г.

Обработка исключений и корректность программ на С++.

Я снова хочу затронуть тему обрабртки исключений. На этот раз речь пойдет о структурной обработке исключений операционной системы windows — SEH. Стандарт описывает только модель обработки исключений, не зависящую от платформы, под которую программа компилируется. Такое исключение может быть выброшено с помощью ключевого слова throw, функцией стандартной библиотеки, или оператором new... Исключение имеет тип, который определяет то, какой обработчик будет вызван, а так-же то, какую информацию исключение передаст в свой обработчик. Помимо этого существуют еще структурные исключения, они не зависят от языка программирования, а специфичны для операционной системы. Такое исключение может быть выброшено при попытке поделить на ноль, или ошибке доступа к памяти и так далее. Компилятор Visual Studio имеет такую опцию как /Eha, которая позволяет программе использовать SEH. Использовать одновременно исключения обоих типов, в программе на С++ проблематично, так как прийдется их обрабатывать по отдельности. Что-бы этого избежать SEH исключение нужно транслировать в обычное исключение. Делается это с помощью функции _set_se_translator стандартной библиотеки, сама эта функция стандартной не является. Она получает указатель на функцию транслятор, которая получает структуру описывающую исключение и в ответ, должна бросить типизированное исключение, вот как-то так:
#include <exception>
#include <iostream>
#include <eh.h>
#include <windows.h>
void trans_func( unsigned int u, EXCEPTION_POINTERS* p )
{
    std::cout << "trans_func called" << std::endl;
    if (p->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
        throw std::runtime_error("access violation");
    throw std::runtime_error("some other error");
}
class Foo 
{
public:
    virtual void bar() { std::cout << ":)" << std::endl; }
};
int main(int argc, char* argv[])
{
    _set_se_translator(&trans_func);
    Foo *foo = NULL;
    try 
    {
        foo->bar();
    }
    catch(std::exception& e) 
    {
        std::cout << "error handled: " << e.what() << std::endl;
    }
    system("pause");
    return 0;
}
Этот код выведет сообщение об ошибке. Теперь о моральных аспектах проблеммы. Во первых, использование опции /Eha снижает общую производительность, так как компилятор «не знает» о том, где может быть выброшено исключение, а где его в принципе быть не может. Во вторых выбрасывание исключения при разименовании указателя или при порче стэка — не стандартное поведение, к примру разименование нулевого указателя, это undefined behavior. Поэтому программа, которую я привел в качестве примера, не является корректной с точки зрения стандарта, но зато работает :).

вторник, 4 марта 2008 г.

Дизайн класса исключения

В большинстве случаев от класса исключения не требуется больше, чем то что уже есть в классе std::exception, то что я напишу дальше можно и не читать Мне недавно понадобилось привязать к исключению произвольные данные, строку, или число, или адрес из контекста в котором произошло исключение. До этого я пользовался обычным, унаследованным от std::exception классом. Самым приемлемым для меня вариантом оказалось использование вариантных данных. Задавать тип данных шаблонным параметром оказалось не удобно, точнее неудобно оказалось потом обрабатывать такие исключения, так как код, обрабатывающий исключение должен знать данные какого типа содержит исключение. В общем как-то так:
template<typename T>
class my_exception_t : public std::exception
{
    T meta_data_;

....

try
{
    do_something_hazardous();
} catch(???)
{
    обработка исключения
}
в блоке catch можно конечно ловить std::exception, но тогда нельзя получить доступ к метаданным исключения, иначе нужно знать как инстанциируется my_exception_t. Альтернативный вариант - определять тип метаданных в рантайме. Для этого я выбрал boost::any, boost::variant нужно собирать, потом добавлять в каждый проект lib, что очень лень)) Получилось такое:
using boost::any_cast;
using boost::any;

class EFailure : public std::exception
{
    any error_code;
public:
    template <typename Param>
    EFailure(Param ec, const char* m) : std::exception(m), error_code(ec) 
    {
    }
    EFailure(const char* m) : std::exception(m), error_code() 
    {
    }
    EFailure(const EFailure& e) : std::exception(e), error_code(e.error_code)
    {
    }
    virtual any get_error_code() const {return error_code;}
};
Ну а для вывода всего этого в поток пришлось сделать страшное))
namespace 
{
    typedef boost::tuple<int, unsigned int, long, unsigned long, double, float, char, unsigned char, std::string> predefined_types;//типы которые может принимать error_code 
    //форматирует error_code и выводит в поток
    template<typename T>
    struct error_code_provider_t;

    template<typename Head, typename Tail>
    struct error_code_provider_t< boost::tuples::cons<Head, Tail> >
    {        
        void operator() (any& value, std::ostream& os)
        {
            if ( value.type() == typeid(Head) )
            {
                os << any_cast<Head>(value);
            } else {
                error_code_provider_t<Tail> next_provider;
                next_provider (value, os);
            }
        }
    };
    template<>
    struct error_code_provider_t<boost::tuples::null_type>
    {
        void operator() (any& value, std::ostream& os)
        {
            os << "can't read this value";
        }
    };

};

std::ostream& operator << (std::ostream& os, const EFailure& f)
{
    error_code_provider_t< predefined_types::cons > error_code_provider;
    os << f.what();
    if ( ! f.get_error_code().empty() )
    {
        os << " ["; error_code_provider(f.get_error_code(), os); os << "]" << std::endl;
    }
    return os;
}

пятница, 18 января 2008 г.

Обработка ошибок

Для себя я уяснил 2 простых правила обработки ошибок в программе. Любая необработанная ошибка должна иметь последствия. Неважно какие, главное, что-бы была возможность узнать, что ошибка произошла. Как следствие - второе правило: Должна существовать возможность узнать причину возникновения ошибки. Неважно как, но должна быть возможность установить точное место возникновения ошибки. Варианты - от сообщения в лог, с именем функции и номером строки, до создания дампа, содержащего состояние стека, и регистров в момент обнаружения ошибки. Ну а как-же быть с ошибками, не приводящим к завершению приложения? Для этого есть первое правило %) Разумеется следует пользоваться исключениями и писать безопасный с точки зрения исключений код. Возвращаемый какой нибудь функцией HRESULT, клиентский код может и не проверить, исключение он конечно то-же может и не перехватить, но по крайней мере будет ясно где ошибка. Так-же не стоит увлекаться разработкой разветвленных иерархий классов исключений, вполне хватит и одного (благодаря второму правилу и бритве Оккама :-D)