С++ обработка исключений и идиомы сообщений об ошибках
В С++ RAII часто выступает в качестве превосходного подхода к обработке исключений: если генерируется исключение, стек разматывается, все деструкторы вызываются и ресурсы очищаются.
Однако это создает проблему с сообщением об ошибках. Скажем, что очень общая функция не работает, стек разматывается на верхний уровень, и все, что я вижу в журналах, будет:
Не удалось прочитать из сокета: соединение reset с помощью одноранговой сети.
... или любое одинаково общее сообщение. Это не говорит о контексте, из которого выбрано исключение. Особенно, если я запускаю что-то вроде цикла обработки очереди событий.
Конечно, я мог бы обернуть каждый вызов сокетов с помощью блока try/catch, поймать исключение, построить новый с более подробной информацией о контекстах и повторно бросить его, но он побеждает цель иметь RAII и медленно, но верно ухудшается, чем обрабатывать коды ошибок возврата.
Какой лучший способ для подробного описания ошибок в стандартном С++? Я также открыт для предложений, связанных с Boost.
Ответы
Ответ 1
Как предложил Джеймс Макнеллис, есть действительно опрятный трюк с участием защитного объекта и объекта std::uncaught_exception
.
Идея состоит в том, чтобы написать такой код:
void function(int a, int b)
{
STACK_TRACE("function") << "a: " << a << ", b: " << b;
// do anything
}
И запишите сообщение только в том случае, если на самом деле выбрано исключение.
Класс очень прост:
class StackTrace: boost::noncopyable // doesn't make sense to copy it
{
public:
StackTrace(): mStream() {}
~StackTrace()
{
if (std::uncaught_exception())
{
std::cout << mStream.str() << '\n';
}
}
std::ostream& set(char const* function, char const* file, unsigned int line)
{
return mStream << file << "#" << line << " - " << function << " - ";
}
private:
std::ostringstream mStream;
};
#define STACK_TRACE(func) \
StackTrace ReallyUnwieldyName; \
ReallyUnwieldyName.set(func, __FILE__, __LINE__)
Можно использовать __PRETTY_FUNC__
или эквивалент, чтобы не называть функцию, но на практике я обнаружил, что она слишком искалечена /verbose для моего собственного вкуса.
Обратите внимание, что вам нужен именованный объект, если вы хотите, чтобы он дожил до конца области, что является целью здесь. Мы могли бы подумать о сложных способах создания уникального идентификатора, но я никогда не нуждался в нем, даже если он защищает более узкую область действия внутри функции, правила скрытия имени играют в нашу пользу.
Если вы объедините это с ExceptionManager
(что-то там, где заброшены сами регистры), вы можете получить ссылку на последнее исключение, а в случае ведения журнала вы можете решить настроить свой стек внутри самого исключения. Так что он печатается what
и игнорируется, если исключение отбрасывается.
Это вопрос вкуса.
Обратите внимание, что в присутствии ExceptionManager
вы должны помнить, что не все исключения могут быть получены с ним → только те, которые вы создали самостоятельно. Таким образом, вам по-прежнему нужна мера защиты от std::out_of_range
и сторонних исключений.
Ответ 2
Я никогда не делал этого, но вы могли бы перевернуть свою собственную "stacktrace":
struct ErrorMessage {
const char *s;
ErrorMessage(const char *s) : msg(s) {}
~ErrorMessage() { if (s) std::cout << s << "\n"; }
void done() { s = 0; }
};
void someOperation() {
ErrorMessage msg("Doing the first bit");
// do various stuff that could throw
msg = "Doing the second bit";
// do more stuff that could throw
msg.done();
}
Вы можете иметь несколько уровней этого (хотя и не обязательно использовать его на каждом уровне):
void handleFoo() {
ErrorMessage msg("Handling foo event");
someOperation();
msg.done();
}
И добавьте больше конструкторов и членов:
void handleBar(const BarEvent &b) {
ErrorMessage msg(std::stringstream("Handling bar event ") << b.id);
someOperation();
msg.done();
}
И вам не нужно писать сообщения в std::cout
. Это может быть для какого-то объекта ведения журнала, и объект может поставить их в очередь, а не выводить их в журнал, если только сайт catch не сообщает об этом. Таким образом, если вы поймаете исключение, которое не гарантирует регистрацию, ничего не написано.
Это не очень, но это красивее, чем try/catch/throw или проверка возвращаемых значений. И если вы забудете позвонить done
при успехе (например, если ваша функция имеет несколько возвратов и вы пропустите один), вы, по крайней мере, увидите свою ошибку в журналах, в отличие от утечки ресурсов.
[Edit: oh, и с подходящим макросом вы можете хранить __FILE__
и __LINE__
в ErrorMessage
.]
Ответ 3
Скажите, что очень общая функция не работает, стек разматывается на верхний уровень, и все, что я вижу в журналах, будет [...]
Какой лучший способ для подробного описания ошибок в стандартном С++?
Обработка ошибок не является локальной для класса или библиотеки - это проблема уровня приложения.
Лучше всего я могу ответить на ваш вопрос, так это то, что отчет об ошибках всегда должен выполняться, если смотреть прежде всего на обработку ошибок. (И обработка ошибок также включает в себя обработку ошибки пользователем.) Обработка ошибок - это принятие решения о том, что должно быть сделано в отношении ошибки.
Это одна из причин, почему отчет об ошибках является проблемой уровня приложения и сильно зависит от рабочего процесса приложения. В одном приложении "соединение reset одноранговым узлом" является фатальной ошибкой - в другой это норма жизни, ошибка должна быть бесшумно обработана, соединение должно быть восстановлено, а ожидаемая операция повторена.
Таким образом, упомянутый вами подход - поймать исключение, построить новый с более подробной информацией о контекстах и повторно бросить его - также является допустимым: это до логики приложения верхнего уровня (или даже пользовательской конфигурации) до решить, действительно ли ошибка является ошибкой или какое-либо специальное (re) действие должно принять условие.
То, с чем вы столкнулись, является одной из недостатков так называемой обработки ошибок вне очереди (aka exceptions). И я не знаю, как это сделать лучше. Исключения создают дополнительный код в приложении, и если отчет об ошибках имеет жизненно важное значение, то дизайн вторичного кодового пути должен обрабатываться так же, как и основной путь кода.
Очевидной альтернативой обработке ошибок вне очереди является встроенная обработка ошибок - хорошие коды возврата и сообщения журнала в условиях ошибки. Это позволяет использовать трюк, при котором приложение сохраняет все сообщения журнала с низкой степенью важности во внутренний (круговой) буфер (фиксированный или настраиваемый размер) и выгружает их в журнал только в случае ошибки высокой степени серьезности. Таким образом, доступна больше контекстной информации, и разные уровни приложения не должны активно информировать друг друга. Это также стандартное (а иногда и буквально "стандартное" - согласно закону) способ сообщения об ошибках в областях приложений, таких как безопасность и критически важное программное обеспечение, не позволяло пропустить ошибки.
Ответ 4
Вы можете добавить стек вызовов в свое исключение. Я не уверен, насколько это хорошо работает для выпуска, но работает как прелесть с отладкой. Это можно сделать в конструкторе вашего исключения (для его инкапсуляции). См. здесь для начальной точки. Это возможно и в Linux - хотя я не помню, как именно.
Ответ 5
Я использую RAII и исключения и просто использую различные инструкции утверждения assert-like во всем коде - если они терпят неудачу, стек распадается туда, где я могу их поймать и обработать.
#define APP_ASSERT_MSG(Class,Assertion,szDescription) \
if ( !(Assertion) ) \
{ \
throw Class(__LINE__,__FILE__,szDescription); \
}
Для большей части моего конкретного случая использования все, о чем я забочусь, это регистрация информации об отладке, поэтому в моем исключении есть номер файла и строки в нем вместе с сообщением об ошибке (сообщение является необязательным, так как у меня есть утверждение без него), Вы можете легко добавить FUNCTION или код ошибки какого-либо типа для лучшей обработки.
Затем я могу использовать его следующим образом:
int nRet = download_file(...);
APP_ASSERT_MSG(DownloadException == ERR_OK, "Download failed");
Это облегчает обработку ошибок и отчетность.
Для действительно неприятной отладки я использовал инструментарий функции GCC, чтобы сохранить список следов происходящего. Он работает хорошо, но немного замедляет приложение.
Ответ 6
То, что я делаю регулярно, FWIW, не использует исключения, а скорее явную обработку ошибок в стандартном формате (т.е. используя макрос). Например:
result = DoSomething();
CHECK_RESULT_AND_RETURN_ON_ERROR( result );
Теперь, очевидно, это имеет множество ограничений по дизайну:
- Ваши коды возврата могут быть несколько однородными
- Код загроможден макросами
- Вам может понадобиться много макросов для различных условий проверки
Потенциал роста, по мнению Dummy00001, позволяет эффективно генерировать трассировку стека по требованию в случае серьезной ошибки, просто кэшируя результаты. Я также использую эту парадигму для регистрации всех непредвиденных условий ошибки, поэтому я могу настроить код позже, чтобы обрабатывать неожиданные условия, которые происходят "в дикой природе".
Что мой 2c.
Ответ 7
Вот как я обрабатываю отчеты об ошибках в своих библиотеках (это может или не подходит вашей ситуации).
Во-первых, в рамках вашего проекта вам нужна "основная" или "системная" библиотека, в которой будет находиться вся эта общая логика. Все остальные библиотеки будут ссылаться на ядро и использовать свои API отчетов об ошибках, поэтому весь ваш система имеет единый компактный блок логики для обработки этого беспорядка.
Внутри ядра укажите набор логических MACROS, таких как "LogWarning" и "LogFatal" с документированным поведением и ожидаемым использованием, например, LogFatal должен инициировать жесткое прерывание процесса, но все, что ниже "LogError", просто консультативный (ничего не делает). Макросы могут предоставить интерфейс "printf", который автоматически добавляет макросы "LINE", "FILE" и "FUNC" в качестве аргументов базовому одноэлементному объекту, который обрабатывает ваши отчеты об ошибках.
Для самого объекта я отдам вам. Однако вы хотите, чтобы общедоступные API-интерфейсы настраивали ваши "стоки" - например, войдите в stderr, войдите в файл журнала, войдите в журнал служб MS и т.д. Вы также хотите, чтобы базовый синглтон был потокобезопасным, возвращаемым по возможности и очень надежным.
С помощью этой системы вы можете сделать "отчет об исключительных ситуациях" ОДНОМ БОЛЬШЕ ТИПА ЗЕМЛИ. Просто добавьте внутренний API к этому объекту диспетчера ошибок, который упаковывает ваше зарегистрированное сообщение в качестве исключения, а затем выдает его. Затем пользователи могут включить и отключить поведение исключений по ошибке в вашем коде с помощью ONE LINE в своих приложениях. Вы можете поместить пробные уловы вокруг стороннего или системного кода в своих библиотеках, которые затем вызывают макрос "Log..." в предложениях catch, чтобы обеспечить чистое поведение повторного броска (с определенными платформами и параметрами компилятора вы даже можете поймать такие вещи, как segfaults, используя это).