Лучшая практика для отчетов об ошибках с помощью std :: source_location - PullRequest
1 голос
/ 13 июля 2020

Среди новых стандартных средств библиотеки, представленных в C ++ 20, есть std::source_location - класс, который позволяет нам удобно собирать и обрабатывать информацию, которая ранее была доступна только через макросы, определенные реализацией, такие как __FILE__ или __LINE__. Насколько я понимаю, его предполагаемое использование заключается в функциях ведения журнала и отчетов об ошибках. Например, функция, объявленная как

void log(const char* message, std::source_location src_loc = std::source_location::current())

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

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

Скажем, что ошибка всегда возникает в функции foo. Простой способ сообщить об этом будет:

int foo(double x)
{
  // ...
  if (error_occurred)
  {
    log(error_message);
    // shutdown
  }
  // ...
}

Однако это будет сообщать об ошибке с сайта вызова log, который всегда является той же самой строкой из реализации foo. Это не очень полезно, особенно если foo - это функция низкого уровня, которая вызывается несколько раз из разных мест в коде. Было бы более полезно сообщить о местоположении вызова foo, который привел к ошибке. Я хотел бы сравнить два подхода к этой проблеме.

1. Вызывает исключение

Допустим, у нас есть такие функции, как fun_1, fun_2, ... которые вызывают foo. Мы могли бы изменить foo на

int foo(double x)
{
  // ...
  if (error_occurred)
  {
    throw std::runtime_error{error_message};
  }
  // ...
}

, а затем внутри fun_1:

try
{
  n = foo(x);
}
catch (const std::runtime_error& error)
{
  log(error.what());
  // shutdown
}

Таким образом, log сообщит местоположение блока catch, связанного с вызовом foo.

2. Распространение source_location

В качестве альтернативы мы могли бы изменить подпись и тело foo:

int foo(double x, std::source_location src_loc = std::source_location::current())
{
  // ...
  if (error_occurred)
  {
    log(error_message, src_loc);
    // shutdown
  }
  // ...
}

Затем, не делая ничего, кроме n = foo(x); внутри fun_1, мы получаем тот же отчет, что и в подходе 1. (Даже немного лучше, поскольку он указывает непосредственно на вызов foo, а не на блок catch.)

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

  1. Выдача исключения
  • Сайты вызовов foo сильно загрязнены. Если есть много функций, таких как fun_1 и fun_2, возможно, каждая из них вызывает foo несколько раз, и мы хотим точно сообщить, какой из вызовов вызвал ошибку, тогда каждая должна быть оформлена блоками try / catch. .
Распространение source_location
  • Подпись foo загрязнена.
  • Если существует более длинный стек вызовов между foo и fun_1 тогда нам придется загрязнять сигнатуры и вызывать сайты всех промежуточных функций.

Я считаю, что подход 1 является довольно стандартной практикой в ​​C ++, но я не смог найти никакой информации об использовании source_location Я предлагаю. Было бы неплохо? Разумно ли использовать source_location в качестве параметра в функциях, принадлежащих «производственному коду», а не только для специальных функций обработки ошибок? Может ли это привести к каким-либо проблемам, о которых я не замечаю?

1 Ответ

0 голосов
/ 13 июля 2020

Рассмотрите вариант 3

int foo(double x)
{
  // ...
  if (error1_occurred)
  {
    log(error_message, std::source_location::current());
    // shutdown
  } else if (error2_occured) {
    log(error_message, std::source_location::current());
  }
  // ...
}

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

С другой стороны, если вы действительно хотите собрать информацию где-то наверху, рассмотрите вариант 4:

int foo(double x)
{
  // ...
  if (error_occurred)
  {
    throw my_error{error_message,std::source_location::current()};
  }
  // ...
}

Это не очень полезно, особенно если foo - это функция низкого уровня, которая вызывается несколько раз из разных мест кода

Это не отличается от старых __FILE__ и __LINE__ макросы. Они показывают файл / строку, в которой они заменены препроцессором. std::source_location работает аналогично.

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

...