Какая школа создания отчетов о функциональных отказах лучше

Очень часто у Вас есть функция, которая для данных аргументов не может генерировать допустимый результат, или он не может выполнить некоторые задачи. Кроме исключений, которые не являются таким образом наиболее часто используемые в мире 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
}

Какой путь делают Вы предпочитаете и почему. Дополнительный параметр во втором методе приносят известную производительность наверху?

8
задан Lightness Races with Monica 2 January 2012 в 00:52
поделиться

17 ответов

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

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

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

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

15
ответ дан 5 December 2019 в 04:59
поделиться

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

Об этом можно много сказать.

Пример:

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

редактировать

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

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, который можно проверить после сбоя.

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

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

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

1
ответ дан 5 December 2019 в 04:59
поделиться

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

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

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

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

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

1
ответ дан 5 December 2019 в 04:59
поделиться

boost optional - блестящий прием. В этом поможет пример.

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

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;
}
8
ответ дан 5 December 2019 в 04:59
поделиться

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

Что касается C, я считаю, что ускользающие параметры менее оскорбительны, чем ускользающие «магические числа».

1
ответ дан 5 December 2019 в 04:59
поделиться

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

0
ответ дан 5 December 2019 в 04:59
поделиться

Для функции "try", где разумно ожидается какой-то "нормальный" тип сбоя, как насчет принятия либо возвращаемого значения по умолчанию, либо указателя на функцию, которая принимает определенные параметры, связанные с сбоем, и возвращает такое значение ожидаемого типа?

0
ответ дан 5 December 2019 в 04:59
поделиться

Я предпочитаю использовать код возврата для типа возникшей ошибки. Это помогает вызывающей стороне API предпринять соответствующие шаги по обработке ошибки.

Рассмотрим API GLIB, которые чаще всего возвращают код ошибки и сообщение об ошибке вместе с возвращаемым булевым значением.

Таким образом, когда вы получаете отрицательный ответ на вызов функции, вы можете проверить контекст из переменной GError.

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

0
ответ дан 5 December 2019 в 04:59
поделиться

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

0
ответ дан 5 December 2019 в 04:59
поделиться

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

0
ответ дан 5 December 2019 в 04:59
поделиться

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

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

1
ответ дан 5 December 2019 в 04:59
поделиться

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

Еще один прием в Си, который я встречал, - возвращать 0 при успехе, а при ошибке возвращается -1, и errno устанавливается, чтобы указать на ошибку.

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

2
ответ дан 5 December 2019 в 04:59
поделиться

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

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

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

6
ответ дан 5 December 2019 в 04:59
поделиться

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

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

 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 (для примера) выбрали исключительно последнюю форму. IMO, во многом только из-за этого решения практически весь код, использующий родной COM API напрямую, уродлив и почти нечитабелен. Задействованные концепции не особенно сложны, но стиль интерфейса превращает то, что должно быть простым кодом, в почти нечитаемый беспорядок практически в каждом случае.

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

4
ответ дан 5 December 2019 в 04:59
поделиться

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

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

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

Да! Дополнительные параметры заставляют ваш 'puter замедляться по крайней мере на 0 наносекунд. Лучше всего использовать ключевое слово "no-overhead" для этого параметра. Это расширение GCC __attribute__((no-overhead)), так что YMMV.

-4
ответ дан 5 December 2019 в 04:59
поделиться

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

0
ответ дан 5 December 2019 в 04:59
поделиться

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

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

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

1
ответ дан 5 December 2019 в 04:59
поделиться