Monday, January 12, 2009

C++: Как поймать опасные исключения

Существует множество мнений по поводу механизма исключений: некоторые называют исключения "скрытым goto", другие же считают исключения отличным механизмом и предлагают его использовать везде и всегда. Единственное, с чем согласны все - исключения нужно использовать с особой осторожностью. Ведь даже с виду совсем безобидный код может привести к тому, что ваше приложение упадёт в самый неподходящий момент.
С++, как многим известно, предоставляет некий механизм спецификации исключений - exceptions spectifications(п.15.4), простейший пример которого представлен ниже:

void foo() throw(Exception1, Exception2)
{
//...
}
В этом небольшом примере функцию foo описывают как функцию, которая может бросить исключения Exception1 и Exception2. Однако, как пишут в Boost Requirements and Guidelines:
The biggest problem with exception-specifications is that programmers use them as though they have the effect the programmer would like, instead of the effect they actually have.
или
Основная проблема со спецификацией исключений в том, что программисты используют их, как будто они работают так, как этого хочет программист, а не так, как они действуют на самом деле
Действительно, попробуйте в функции foo бросить исключение Exception3 и скомпилировать при помощи GCC(другие компиляторы я не использую, но думаю, что они действуют также). Ничего не произошло? И у меня то же самое :)
Т.е., спецификация исключений - вещь относительно бесполезная, а иногда и вредная(некоторые компиляторы не инлайнят функции с exception specifications). Существует много тредов-обсуждений в листе рассылки GCC по поводу возможности вывода предупреждений о несоответствующем спецификации использовании исключений(вот это сообщение - самое полезное, по-моему). В конце концов, подобные запросы отклоняются, ибо "невозможно реализовать из-за сложности межпроцедурного анализа. GCC не предназначен для такого".
Однако, это не остановило проект EDoc++, о котором и хотелось бы рассказать подробней. Он использует пропатченный GCC для анализа кода на ошибки при использовании исключений. Например, у нас есть код:
#include <iostream>
class C1{};
class C2{};

void foo() throw (C1)
{
throw C2();
};

class C
{
public:
~C()
{
foo();
}
};

int main()
{
C c;
}

В этом примере есть несколько ошибок, связанных с исключениями:
  • Не соблюдается спецификация в функции foo
  • Бросается исключение из деструктора
  • Исключение выходит за функцию main, что приводит к аварийному завершению программы
Попробуем проанализировать наш код с использованием EDoc++. Сначала EDoc++ надо собрать, потом проинициализировать окружение. В итоге мы получаем bash-оболочку с приглашением "EDOC ->". Я сохранил вышеприведенный С++ файл в ~/dev/sandbox/t.cpp. Компилируем(все дополнительные опции смотрите в документации к EDoc++):
EDOC -> g++ -fedoc-source -c ~/dev/sandbox/t.cpp -o ~/dev/sandbox/t.o
EDoc++ создает дополнительный файлик, который при опции -fedoc-source попадает в ~/dev/sandbox/t.cpp.edc. Показываем полученные результаты:
EDOC -> edoc --show-all --format simple ~/dev/sandbox/t.cpp.edc 
F: _GLOBAL__D__Z3foov()

F: _GLOBAL__I__Z3foov()

F: foo()

F: __static_initialization_and_destruction_0(int, int)

F: C::~C()

F: main()

ERROR(ECRASH_SPEC): Exception may propogate through restrictive specifier :The function: foo() can throw an exception of type: C2 that is not allowed by its specifier list: throws(C1)
Кажется, лучше сообщение об ошибке и не укажешь. Исправим ошибку - будем делать throw C1; вместо throw C2;. Перекомпилируем и посмотрим:
EDOC -> edoc --show-all --format simple ~/dev/sandbox/t.cpp.edc 
[...skip...]
ERROR(EMAIN_EXC): An exception may propogate out the main function. :The exception: C1 may propagate out the main function which may cause a program abort.

WARNING(WDESTR_EXC): An exception may propogate through a destructor. :Destructor: C::~C(), Exception: C1
Ага! Мы чуть-чуть не упустили исключение за пределы деструктора. Это ужасно. Уберём наконец throw С1; из foo для того чтобы проверить, как EDoc++ отреагирует на спецификацию без реального выбрасывания исключений:
EDOC -> edoc --show-all --format simple ~/dev/sandbox/t.cpp.edc
[...skip...]
Молчит. Т.е., он не принимает во внимание спецификации, если эти спецификации врут, что, в принципе, правильно.
EDoc++ обладает еще множеством достоинств: он поддерживает множество форматов вывода, множество способов хранения .edc информации, судя по отличной документации он правильно работает с указателями на функции, которые бросают исключения, и умеет находить ошибки даже в тех случаях, когда они делятся между несколькими модулями компиляции. В общем, отличный инструмент, спасибо создателям :)

2 comments:

  1. он не принимает во внимание спецификации, если эти спецификации врут, что, в принципе, правильно.
    может я и ошибаюсь, но по моему спецификация исключений указывает на то какие исключение функция может бросать, а не будет бросать :). Ага вот из текущего драфта - 15.4.1 - A function declaration lists exceptions that its function might directly or indirectly throw by using an exception-specification as a suffix of its declarator.

    И отдельное спасибо за ссылку на тул :)

    ReplyDelete
  2. @Yuriy Volkov:
    В данном случае функция ну никак не может бросить исключение, описанное в спецификаии, потому я и написал, что спецификация врёт. Положительная сторона EDoc++ в том, что создатели правильно понимают стандарт и не полагаются на спецификации при поиске ошибок. Отрицательная сторона - хотелось бы получить хинт по тому, что в спецификации указан лишний exception. Вероятно, это настраиваемо

    ReplyDelete