суббота, 22 сентября 2012 г.

Диагностика исключений

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

Короткий обзор

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

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

Все instance отправляются в коллектор и в дальнейшем при перехвате используются по усмотрению.

Детальный обзор

Рассмотрим нижеследующую функцию.

Она занимается тем, что парсит файл специального формата, проверяет корректность и укладывает данные в память в соответствии с внутренним представлением. Для иллюстрации и разбора важно только заметить, что она работает с одним файлом и использует другие функции, это get_utfmarker_line(), check_elements(), getline(), check_and_union_ranges().

void
parse_file( const std::string & filename ) 
{
    std::ifstream file( filename.c_str() );
 
    if ( !file.good() )
    {
        throw phone_ranges_error_t( 
            "Can't open file '" + filename + "' to parse." );
    }
 
    // Receive first line and cut marker (if it present).
    elements_t elements = get_utfmarker_line( file );
 
    // Parse all file and collect map.
    ranges_t ranges;
    while( true )
    {
        check_elements( elements, ranges );
 
        ranges_t::const_iterator it = 
            ranges.find( range_t( elements[0], elements[1] ) );
 
        if ( it == ranges.end() )
            ranges[ range_t( elements[0], elements[1] ) ] = elements[2];
 
        if ( file.eof() )
            break;
 
        elements = getline( file );
    }
 
    check_and_union_ranges( ranges );
 
    m_ranges = ranges;
}

Проблема

Представим, что нам необхожимо диагностировать имя файла в случае, если что-то произошло страшное при любом из действий. Другими словами, если произошло исключение, то нам нужно информацию об имени файла сбросить в лог, показать пользователю, отправить оператору или что-то другое.

Как мы это можем сделать?

Ниже два примера, как это может быть реализовано традиционными методами.

Первый: добавить информацию в пролетающие мимо исключения

void
parse_file( const std::string & filename ) 
{
        std::ifstream file( filename.c_str() );

        try
        {
                ...
        }
        catch( const std::runtime_error & ex )
        {
                throw std::runtime_error( 
                        ex.what() + " parse_file filename:" + filename + ";" );
        }
 
}

Этот способ имеет ряд недостатков.

Во-первых, нам нужно перехватить все типы исключений. Из-за этого требуется добавлять много catch'ей, для каждого из типов. В результате мы обязательно какой-нибудь из них забудем. Кроме того, мы не сможем перехватить неизвестные для нас исключения (например из других библиотек).

Во-вторых, нам нужно писать try и catch. И все содержимое функции сдвигать на одну табуляцию. Все это усложняет код, его понимание и сопровождение.

В-третьих, здесь происходит дополнительная операция конструирования объекта исключения.

Второй: передача параметра в контекст броска исключения

Один из способов:

void
parse_file( const std::string & filename ) 
{
        std::ifstream file( filename.c_str() );
 
...
 
        elements_t elements = get_utfmarker_line( file, filename );

...
 
                check_elements( elements, ranges, filename );
 
...

                elements = getline( file, filename );

...
 
        check_and_union_ranges( ranges, filename );

...
}

Здесь следующие проблемы.

Один дополнительный параметр для каждой функции. Из-за чего код становится более сложным для восприятия и сопровождения. Мы должны контролировать каждую функцию, и какую-нибудь из них обязательно забудем. Кроме передачи параметра мы должны в каждой точке броска исключения дописать информацию в throw.

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

Предлагаемое решение

Необходимо добавить всего одну строчку:

void
parse_file( const std::string & filename ) 
{
        ex_diag::reg<std::string> reg_file ( filename, "parse filename" );
...

В конечных точках перехвата мы можем использовать всю диагностическую информацию, которая была собрана автоматически. Это можно разобрать вручную (какое бы то ни было исключение):

catch ( ... )
        {
                std::cout << 
                        ex_diag::get_collector_instance().info() << std::endl;
        }

Или это произойдет само, если мы наследовались от исключения библиотеки:

catch ( const ex_diag::ex_t & ex )
        {
                // Dump will be at ex_diag::ex_t d'tor.
        }

В завершение

Инструмент межплатформенный, проверенный, с тестами и примерами.

Проверялся с помощью VC7 и GCC.

Обертки работают для любых объектов, которые можно отправить в std::ostream.

Проектировалось для минимального синтаксиса и минимального использования ресурсов.