Выложил на всеобщее обозрение простой инструмент диагностики объектов при падениях в случае исключений.
Короткий обзор
Во время возникновения исключений часто возникает ситуация, что хотелось бы знать более детальную информацию о состоянии системы во время ошибки. Чаще всего информации в конечной точке не хватает, а диагностика более подробной черевата сложностями в сопровождении и отладке.
Идея заключается в том, чтобы диагностировать состояние всех разрушающихся объектов на пути исключения. На каждый объект, подлежащий диагностике, делается специальная обертка, в случае разрушения которой во время исключения информация об объекте не теряется, а отправляется в заданный поток. Так это выглядит графически:
Все 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.
Проектировалось для минимального синтаксиса и минимального использования ресурсов.