Какая школа отчетности о сбоях функций лучше - PullRequest
8 голосов
/ 08 июля 2010

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

Первый подход смешивает допустимые возвращаемые значения со значением, которое не принадлежит кодомену функцииочень часто -1) и указывает на ошибку

int foo(int arg) {
    if (everything fine)
        return some_value;
    return -1; //on failure
}

Подход scond заключается в возврате состояния функции и передаче результата в пределах ссылки

bool foo(int arg, int & result) {
     if (everything fine) {
         result = some_value;
         return true;
     }
     return false;  //on failure
}

Какой способ вы предпочитаете и почему,Приносит ли дополнительный параметр во втором методе ощутимые потери производительности?

Ответы [ 17 ]

14 голосов
/ 08 июля 2010

Не игнорируйте исключения, для исключительных и неожиданных ошибок.

Однако, просто отвечая на ваши вопросы, вопрос в конечном итоге субъективен.Ключевой вопрос заключается в том, чтобы подумать, с чем вашим потребителям будет легче работать, в то же время тихо подталкивая их не забывать проверять условия ошибок.На мой взгляд, это почти всегда «вернуть код состояния и поместить значение в отдельную ссылку», но это полностью личное мнение человека.Мои аргументы для этого ...

  1. Если вы решите вернуть смешанное значение, значит вы перегружены понятием возврата, означающим "либо полезное значение , либо код ошибки".Перегрузка единой семантической концепции может привести к путанице в отношении того, что с ней делать правильно.
  2. Часто вы не можете легко найти значения в домене функции, которые можно было бы использовать в качестве кодов ошибок, и поэтому необходимо смешивать и сопоставлятьдва стиля сообщений об ошибках в одном API.
  3. Почти нет шансов, что, если они забудут проверить состояние ошибки, они будут использовать код ошибки, как если бы это был действительно полезный результат.Можно вернуть код ошибки и вставить в возвращаемую ссылку какое-либо нулевое понятие, которое легко взорвется при использовании.Если использовать модель смешанного возврата «ошибка / значение», очень легко передать ее в другую функцию, в которой ошибочная часть совместного домена является допустимым вводом (но не имеет смысла в контексте).

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

8 голосов
/ 08 июля 2010

повышение опционально - блестящая техника. Пример поможет.

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

double divide(double a, double b){
    return a / b;
}

что делать в случае, если b равно 0;

boost::optional<double> divide(double a, double b){
    if ( b != 0){
        return a / b;
    }else{
        return boost::none;
    }
}

используйте его, как показано ниже.

boost::optional<double> v = divide(a, b);
if(v){
    // Note the dereference operator
    cout << *v << endl;
}else{
    cout << "divide by zero" << endl;
}
6 голосов
/ 08 июля 2010

Идея специальных возвращаемых значений полностью разваливается, когда вы начинаете использовать шаблоны. Рассмотрим:

template <typename T>
T f( const T & t ) {
   if ( SomeFunc( t ) ) {
      return t;
   }
   else {         // error path
     return ???;  // what can we return?
   }
}

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

4 голосов
/ 08 июля 2010

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

Хотя я сочувствую этому понятию, я обнаружил, что первое обычно лучше работает на практике. Для одного очевидного момента, в первом случае вы можете связать назначение с произвольным числом получателей, но во втором, если вам нужно / хотите назначить результат более чем одному получателю, вы должны выполнить вызов, а затем выполнить отдельно. второе назначение. Т.е.

 account1.rate = account2.rate = current_rate();

против:.

set_current_rate(account1.rate);
account2.rate = account1.rate;

или

set_current_rate(account1.rate);
set_current_rate(account2.rate);

Доказательство пудинга в его поедании. COM-функции Microsoft (для одного примера) выбрали только последнюю форму. ИМО, во многом это связано только с этим решением, что по существу весь код, который напрямую использует собственный COM API, является уродливым и почти нечитаемым. Используемые концепции не особенно сложны, но стиль интерфейса превращает простой код в практически нечитаемый беспорядок практически в каждом случае.

Обработка исключений обычно лучший способ обработки вещей, чем любой из них. У этого есть три определенных эффекта, все из которых очень хороши. Во-первых, он предотвращает загрязнение основной логики обработкой ошибок, поэтому реальная цель кода намного яснее. Во-вторых, он отделяет ошибку обработку от ошибки обнаружение . Код, который обнаруживает проблему, часто находится в плохом положении, чтобы обрабатывать эту ошибку очень хорошо. В-третьих, в отличие от любой формы возврата ошибки, по сути невозможно просто игнорировать создаваемое исключение. При использовании кодов возврата существует почти постоянный соблазн (которому программисты слишком часто уступают) просто предполагать успех и не пытаться даже обнаружить проблему - тем более, что программист на самом деле не знает, как обрабатывать ошибку при этом. часть кода в любом случае, и хорошо понимает, что даже если он перехватит его и вернет код ошибки из своей функции, велика вероятность, что it все равно будет проигнорировано.

2 голосов
/ 10 июля 2010

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

Другой метод C, который я вижу, - это возврат0 в случае успеха и в случае ошибки, возвращается -1, и для указания ошибки устанавливается errno.

Каждый из представленных вами методов имеет свои плюсы и минусы, поэтому выбор того, какой из них «лучший», всегда будетхотя бы частично) субъективно.Тем не менее, я могу сказать это без оговорок: лучшая техника - это техника, согласованная на протяжении всей вашей программы .Использование различных стилей кода сообщения об ошибках в разных частях программы может быстро стать кошмаром для обслуживания и отладки.

1 голос
/ 08 июля 2010

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

При использовании C ++ существует гораздо больше возможностей, чем эти два, включая исключения и использование чего-то вроде boost :: option в качестве возвращаемого значения.

1 голос
/ 08 июля 2010

Вы пропустили метод: возвращаете индикацию ошибки и запрашиваете дополнительный вызов для получения подробностей об ошибке.

Для этого многое можно сказать.

Пример:

int count;
if (!TryParse("12x3", &count))
  DisplayError(GetLastError());

edit

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

HKEY key;
long errcode = RegOpenKey(HKEY_CLASSES_ROOT, NULL, &key);
if (errcode != ERROR_SUCCESS)
  return DisplayError(errcode);

Сравните это с:

HKEY key;
if (!RegOpenKey(HKEY_CLASSES_ROOT, NULL, &key))
  return DisplayError(GetLastError());

(версия GetLastError согласуется с тем, как Windows API обычно работает, но версия, которая возвращает код напрямую, такова, как он работает на самом деле, из-за того, что API реестра не следует этому стандарту.)

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

HKEY key;
if (RegOpenKey(HKEY_CLASSES_ROOT, NULL, &key) != ERROR_SUCCESS)
  return DisplayGenericError();

edit

Просмотр запроса R.Я нашел сценарий, в котором он действительно может быть удовлетворен.

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

Однако, если мы расширим тему, чтобы включить методы в классе, ситуацияdiffereнт.Совершенно разумно, учитывая переменную reg, которая является экземпляром класса RegistryKey, для вызова reg.Open для возврата false, требующего от нас затем вызвать reg.ErrorCode для получения подробностей.

Я полагаю, что это удовлетворяет запросу Р. о том, что код ошибки должен быть частью контекста, поскольку экземпляр обеспечивает контекст.Если вместо RegistryKey экземпляра мы вызовем статический Open метод для RegistryKeyHelper, то извлечение кода ошибки при сбое также должно быть статическим, что означает, что это должен быть TLV, хотяне совсем глобальный.Класс, в отличие от экземпляра, будет контекстом.

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

1 голос
/ 08 июля 2010

Для C ++ я предпочитаю шаблонное решение, которое предотвращает ошибочность выходных параметров и неопределённость "магических чисел" в комбинированных ответах / кодах возврата.Я объяснил это, пока отвечал на другой вопрос .Взгляните.

Для C я нахожу неопрятные параметры менее оскорбительными, чем неопрятные "магические числа".

1 голос
/ 08 июля 2010

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

  • Вместо того, чтобы передавать имя файла вглубь многих вызовов функций, вы могли бы спроектировать свою программу так, чтобы вызывающая программа открывала файл и передавала дескриптор FILE * или файл.Это исключает проверки «не удалось открыть файл» и сообщать об этом вызывающей стороне на каждом этапе.

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

  • Когда функция выполняет задачу, для которой ваша реализация можеттребуется большое рабочее пространство, спросите, есть ли альтернативный (возможно, более медленный) алгоритм с O(1) требованиями к пространству.Если производительность не критична, просто используйте пробел O(1).В противном случае, используйте запасной вариант, чтобы использовать его в случае неудачного размещения.

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

1 голос
/ 08 июля 2010

C традиционно использовал первый подход кодирования магических значений в правильных результатах - вот почему вы получаете забавные вещи, такие как strcmp (), возвращающие false (= 0) для совпадения.

Более новые безопасные версии множестваиз стандартных функций библиотеки используется второй подход - явное возвращение статуса.

И никакие исключения здесь не являются альтернативой.Исключения для исключительных обстоятельств, с которыми код может не справиться - вы не вызываете исключение для строки, не совпадающей в strcmp ()

...