Очень часто у Вас есть функция, которая для данных аргументов не может генерировать допустимый результат, или он не может выполнить некоторые задачи. Кроме исключений, которые не являются таким образом наиболее часто используемые в мире 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
}
Какой путь делают Вы предпочитаете и почему. Дополнительный параметр во втором методе приносят известную производительность наверху?
Не игнорируйте исключения для исключительных и неожиданных ошибок.
Однако, просто отвечая на ваши вопросы, вопрос в конечном итоге субъективен. Ключевой вопрос - подумать, с чем будет легче работать вашим потребителям, и при этом незаметно подтолкнуть их не забывать проверять условия ошибок. На мой взгляд, это почти всегда «Вернуть код состояния и поместить значение в отдельную ссылку», но это полностью личное мнение каждого человека. Мои аргументы в пользу этого ...
Аргументом в пользу возврата модели смешанного кода / значения ошибки может быть простота - например, никаких дополнительных переменных.Но для меня опасность хуже, чем ограниченный выигрыш - можно легко забыть проверить коды ошибок. Это один из аргументов в пользу исключений - вы буквально не можете забыть обработать их (ваша программа загорится, если вы этого не сделаете).
Вы пропустили метод: возврат сообщения об ошибке и требование дополнительного вызова для получения сведений об ошибке.
Об этом можно много сказать.
Пример:
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, хотя и не полностью глобальный. Класс, в отличие от экземпляра, будет контекстом.
В обоих случаях ориентация объекта обеспечивает естественный контекст для хранения кодов ошибок. Сказав это, если нет естественного контекста, я все равно буду настаивать на глобальном, а не пытаться заставить вызывающего абонента передать выходной параметр или какой-либо другой искусственный контекст или напрямую вернуть код ошибки.
Это не всегда возможно, но независимо от того, какой метод сообщения об ошибках вы используете, лучшей практикой является, по возможности, проектирование функции так, чтобы в ней не было случаев отказа, а когда это невозможно, минимизация возможных условий ошибки. Некоторые примеры:
Вместо того чтобы передавать имя файла через множество вызовов функций, вы можете спроектировать свою программу так, чтобы вызывающая сторона открывала файл и передавала FILE *
или дескриптор файла. Это позволит избежать проверок на "не удалось открыть файл" и сообщать об этом вызывающей стороне на каждом шаге.
Если есть недорогой способ проверить (или найти верхнюю границу) объем памяти, который функция должна будет выделить для структур данных, которые она создаст и вернет, предоставьте функцию для возврата этого объема и поручите вызывающей стороне выделить память. В некоторых случаях это может позволить вызывающей стороне просто использовать стек, значительно уменьшая фрагментацию памяти и избегая блокировок в malloc
.
Когда функция выполняет задачу, для которой вашей реализации может потребоваться большое рабочее пространство, спросите, нет ли альтернативного (возможно, более медленного) алгоритма с O(1)
требованиями к пространству. Если производительность не критична, просто используйте алгоритм с O(1)
пространством. В противном случае реализуйте запасной вариант, чтобы использовать его в случае неудачного распределения.
Это всего лишь несколько идей, но применение одного и того же принципа повсеместно может реально сократить количество ошибок, с которыми приходится иметь дело и которые распространяются по нескольким уровням вызова.
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;
}
Для C ++ я предпочитаю шаблонное решение, которое предотвращает непостоянство выходных параметров и непостоянства «магических чисел» в комбинированных ответах / кодах возврата. Я изложил это, когда отвечал на другой вопрос . Взглянем.
Что касается C, я считаю, что ускользающие параметры менее оскорбительны, чем ускользающие «магические числа».
Если у вас есть ссылки и тип bool, вы должны использовать C ++. В этом случае вызовите исключение. Вот для чего они нужны. Для обычной среды рабочего стола нет причин использовать коды ошибок. Я видел аргументы против исключений в некоторых средах, таких как изворотливое взаимодействие языка / процессов или жесткая встроенная среда. Предполагая, что ни один из них не всегда вызывает исключение.
Для функции "try", где разумно ожидается какой-то "нормальный" тип сбоя, как насчет принятия либо возвращаемого значения по умолчанию, либо указателя на функцию, которая принимает определенные параметры, связанные с сбоем, и возвращает такое значение ожидаемого типа?
Я предпочитаю использовать код возврата для типа возникшей ошибки. Это помогает вызывающей стороне API предпринять соответствующие шаги по обработке ошибки.
Рассмотрим API GLIB, которые чаще всего возвращают код ошибки и сообщение об ошибке вместе с возвращаемым булевым значением.
Таким образом, когда вы получаете отрицательный ответ на вызов функции, вы можете проверить контекст из переменной GError.
Неудача во втором указанном вами подходе не поможет вызывающей стороне предпринять правильные действия. Другое дело, когда ваша документация очень ясна. Но в других случаях это будет головная боль - найти, как использовать вызов API.
Я думаю, что хороший компилятор будет генерировать почти тот же код, с той же скоростью. Это личное предпочтение. Я бы пошел первым.
Я думаю, что на это нет правильного ответа. Это зависит от ваших потребностей, от общего дизайна приложения и т. Д. Хотя лично я использую первый подход.
Между ними не должно быть большой разницы в производительности, если таковая имеется. Выбор зависит от конкретного использования. Вы не можете использовать первый, если нет соответствующего значения invalid.
При использовании C++ возможностей гораздо больше, чем у этих двух, включая исключения и использование чего-то вроде boost::optional в качестве возвращаемого значения.
В Си одна из наиболее распространенных техник, которую я видел, заключается в том, что функция возвращает ноль при успехе, ненулевое значение (обычно код ошибки) при ошибке. Если функции нужно передать данные обратно вызывающему пользователю, она делает это через указатель, переданный в качестве аргумента функции. Это также может сделать функции, возвращающие пользователю несколько частей данных, более простыми в использовании (по сравнению с возвратом некоторых данных через возвращаемое значение и некоторых через указатель).
Еще один прием в Си, который я встречал, - возвращать 0 при успехе, а при ошибке возвращается -1, и errno
устанавливается, чтобы указать на ошибку.
У каждой из представленных вами техник есть свои плюсы и минусы, поэтому решение о том, какая из них "лучше", всегда будет (по крайней мере, частично) субъективным. Однако я могу сказать следующее без оговорок: лучшая техника - это техника, которая последовательна во всей вашей программе. Использование разных стилей кода сообщения об ошибках в разных частях программы может быстро превратиться в кошмар сопровождения и отладки.
Идея специальных возвращаемых значений полностью разваливается, когда вы начинаете использовать шаблоны. Рассмотрим:
template <typename T>
T f( const T & t ) {
if ( SomeFunc( t ) ) {
return t;
}
else { // error path
return ???; // what can we return?
}
}
В данном случае нет очевидного специального значения, которое мы могли бы вернуть, поэтому единственным выходом является выброс исключения. Возвращение булевых типов, которые необходимо проверять, и передача действительно интересных значений обратно по ссылке приводит к ужасному стилю кодирования...
Довольно много книг и т.д. настоятельно советуют второе, чтобы не смешивать роли и не заставлять возвращаемое значение нести две совершенно несвязанные части информации.
Хотя я симпатизирую этой идее, я нахожу, что первый вариант обычно работает лучше на практике. По одному очевидному моменту, в первом случае вы можете связать цепочкой присвоение произвольному количеству получателей, а во втором, если вам нужно/хочется присвоить результат более чем одному получателю, вы должны выполнить вызов, а затем отдельно выполнить второе присвоение. То есть,
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 напрямую, уродлив и почти нечитабелен. Задействованные концепции не особенно сложны, но стиль интерфейса превращает то, что должно быть простым кодом, в почти нечитаемый беспорядок практически в каждом случае.
Обработка исключений - обычно лучший способ решения проблем, чем любой из них. Она имеет три конкретных эффекта, каждый из которых очень хорош. Во-первых, это позволяет не загрязнять основную логику обработкой ошибок, так что реальный смысл кода становится гораздо более ясным. Во-вторых, это отделяет обработку ошибок от обнаружения ошибок. Код, который обнаруживает проблему, часто находится в плохом положении для обработки этой ошибки. В-третьих, в отличие от любой формы возврата ошибки, просто проигнорировать брошенное исключение по сути невозможно. С кодами возврата существует почти постоянное искушение (которому программисты поддаются слишком часто) просто предположить успех и не предпринимать никаких попыток даже поймать проблему - особенно потому, что программист не знает, как обрабатывать ошибку в этой части кода, и прекрасно понимает, что даже если он поймает ее и вернет код ошибки из своей функции, велики шансы, что она все равно будет проигнорирована.
Кроме правильного способа, какой из этих двух глупых способов вы предпочитаете?
Я предпочитаю использовать исключения, когда я использую C++ и мне нужно бросить ошибку, и вообще, когда я не хочу заставлять все вызывающие функции обнаруживать и обрабатывать ошибку. Я предпочитаю использовать глупые специальные значения, когда есть только одно возможное состояние ошибки, и это состояние означает, что вызывающая функция не может продолжать, и все мыслимые вызывающие функции смогут обработать ее... что бывает редко. Я предпочитаю использовать глупые параметры при модификации старого кода, и по какой-то причине я могу изменить количество параметров, но не изменить возвращаемый тип, или определить специальное значение, или выбросить исключение, чего до сих пор никогда не было.
Приносит ли дополнительный параметр во втором методе приносит заметный заметное увеличение производительности?
Да! Дополнительные параметры заставляют ваш 'puter замедляться по крайней мере на 0 наносекунд. Лучше всего использовать ключевое слово "no-overhead" для этого параметра. Это расширение GCC __attribute__((no-overhead))
, так что YMMV.
Ну, первый вариант компилируется как на C, так и на C++, так что для переносимого кода он подходит. Второй вариант, хотя он более "человекочитаемый", вы никогда не знаете достоверно, какое значение возвращает программа, указание его как в первом случае дает вам больше контроля, вот что я думаю.
C традиционно использовал первый подход к кодированию магических значений в действительных результатах - вот почему вы получаете забавные вещи, такие как strcmp (), возвращающая false (= 0) при совпадении.
Более новые безопасные версии многих стандартных библиотечных функций используют второй подход - явное возвращение статуса.
И никакие исключения здесь не являются альтернативой. Исключения составляют исключительные обстоятельства, с которыми код может не справиться - вы не вызываете исключение для строки, не соответствующей в strcmp ()