С++ обработка исключений и идиомы сообщений об ошибках

В С++ 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, используя это).