Какая школа отчетности о сбоях функций лучше

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

Первый подход смешивает допустимые возвраты со значением, которое не принадлежит codomain функции (очень часто -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
}

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


person doc    schedule 08.07.2010    source источник
comment
Исключения обычно используются в C ++. Я бы сказал, что они встречаются чаще, чем коды ошибок.   -  person Staffan    schedule 08.07.2010
comment
Исключения обычно используются в C ++, но они недоступны в C. Так что говорить о мире C / C ++ на самом деле неуместно.   -  person Fred Larson    schedule 08.07.2010
comment
Это довольно новая функция. Многие достойные библиотеки C ++, например, Qt, не используют исключения.   -  person doc    schedule 08.07.2010
comment
нет мира C / C ++. Есть мир C и есть мир C ++. Исключения - это не новые возможности.   -  person Nikko    schedule 08.07.2010
comment
@Fred: если C / C ++ не ссылается на урезанную оболочку языка, которую вы получаете, когда берете C ++ и отключаете половину языковых функций (начиная с исключений) из-за ошибочного убеждения, что они неэффективны. К сожалению, это довольно распространенное явление при разработке встроенного программного обеспечения.   -  person Mike Seymour    schedule 08.07.2010
comment
@doc: Это довольно новая функция: я считаю, что они были введены в C ++ версии 4 в 1993 году. Они, безусловно, хорошо себя зарекомендовали к тому времени, когда язык был стандартизирован в 1998 году.   -  person Mike Seymour    schedule 08.07.2010
comment
@Nikko: Одна из лучших особенностей C ++ - это совместимость с существующим кодом C. Вы теряете это, когда называете C и C ++ двумя разными мирами, и теряете возможность взаимодействия с любым другим языком, когда используете исключения. Это само по себе является вполне законной причиной для выбора другого механизма сообщения об ошибках, а не исключений.   -  person Ben Voigt    schedule 08.07.2010
comment
@Mike: хотя вы в целом правы насчет урезанных подмножеств C ++, есть законные причины для запрета исключений во встроенном коде. Исключения требуют МНОГО дополнительного вспомогательного библиотечного кода, дополнительного пространства стека и затрудняют прогнозирование времени выполнения. Это не проблема для компьютера общего назначения (где кэш уже затрудняет прогнозирование времени), но во встроенной электронике исключения создают больше проблем, чем они того стоят.   -  person Ben Voigt    schedule 08.07.2010
comment
@Mike: Обработка исключений была в ARM, которая обычно считается версией 2.0 (т. Е. Одновременно с CFront 2.0). Он был опубликован в 1990 году.   -  person Jerry Coffin    schedule 08.07.2010
comment
@Jerry Cfront 2.0 не поддерживает исключения.   -  person    schedule 08.07.2010
comment
@Neil: в то время это все еще считалось экспериментальным, но оно было там. См .: softwarepreservation.org/projects/c_plus_plus/cfront/, стр. 97. На самом деле это была опечатка / размышление - на самом деле ARM была 2.1, а не 2.0.   -  person Jerry Coffin    schedule 08.07.2010
comment
@Jerry Не согласно Страуструпу из книги D&E. И не в моем воспоминании как пользователя Cfront 2.0.   -  person    schedule 08.07.2010
comment
stackoverflow.com/questions/2789987/   -  person Nick Van Brunt    schedule 09.07.2010


Ответы (17)


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

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

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

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

person Adam Wright    schedule 08.07.2010
comment
+1 - при условии, что вы используете исключения только для действительно исключительных случаев. Их размотка и очистка - дорогостоящая операция (относительно). Подсказка кроется в названии. - person Ragster; 08.07.2010
comment
Если есть четко определенное понятие недопустимого возвращаемого значения, то такая перегрузка не имеет большого значения. Например, CreateFile возвращает дескриптор NULL в случае ошибки, где NULL всегда считается недопустимым. Когда нет такого понятия, как atoi, возвращающий 0 в случае ошибки, это не так уж и хорошо. - person Steven Sudit; 08.07.2010
comment
Вдобавок некоторые API возвращают код успеха / неудачи при каждом вызове. Проблема в том, что это объединяет работала ли функция с деталями того, как она работала. Например, ODBC возвращает код, в котором положительные значения означают разные типы успеха, а отрицательные - разные типы ошибок. Это тоже нехорошо. - person Steven Sudit; 08.07.2010
comment
@Steven: Думаю, с этим дизайном есть еще одна проблема - использование неправильного магического значения. CreateFile возвращает INVALID_HANDLE_VALUE (~0), а не NULL (0) в случае ошибки. Обычно NULL используется для инициализированных, но неназначенных переменных-дескрипторов, а INVALID_HANDLE_VALUE используется для сбоя. - person Ben Voigt; 08.07.2010
comment
@Ben: Ну, функция определена так, чтобы возвращать дескриптор, а не указатель, поэтому это должен быть недопустимый дескриптор, а не недопустимый указатель. Я определенно ошибся в деталях, когда сказал, что он вернул NULL, но я думаю, что идея была правильной. - person Steven Sudit; 08.07.2010
comment
@ Стивен: Я не пытался оспаривать вашу точку зрения, я делал новую и использовал вас в качестве иллюстрации. При использовании магических значений существует не только риск того, что вызывающий абонент забудет проверить их, но и риск неправильного кодирования теста. Другой пример - сравнение значений COM HRESULT с S_OK вместо использования макроса SUCCEEDED. - person Ben Voigt; 08.07.2010
comment
@Ben: Извини, если я неправильно понял. Да, вы правы в том, что все эти магические значения опасны, потому что мы фактически берем примитивный тип и притворяемся, что он имеет богатую семантику. Конечно, держать дескриптор ОС в том, что по сути (если не на самом деле) является void *, в любом случае - плохой путь. Правильный ответ - обернуть эти возвраты в классы, которые реализуют соответствующие значения деструкторы и средства проверки достоверности. Так, например, класс ComResult, упомянутый в другом месте, действительно использует макрос SUCCEEDED внутри своей функции DidFail. - person Steven Sudit; 08.07.2010
comment
@ Адам Райт: Если есть концепция типа null, которую вы можете вставить в возвращаемую ссылку в случае ошибки, тогда вы могли бы так же легко использовать это значение, как возвращаемое значение ошибки. Если не подходящей концепции типа null, которая взорвется при использовании, тогда у вас точно такая же проблема - вызывающий может проигнорировать ошибку и попытаться использовать значение, которое вы застряли в ссылке. - person caf; 09.07.2010

boost optional - блестящая техника. Пример поможет.

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

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;
}
person bradgonesurfing    schedule 08.07.2010
comment
+1, это было мое чутье, когда я увидел вопрос, я рад, что я не единственный. Это в точности соответствует конструкции Maybe в Haskell и, безусловно, очень естественно. - person Matthieu M.; 08.07.2010
comment
На самом деле, согласно моему пониманию, это не настоящая монада. xtargets.heroku.com/2010/06/03 / using-boostoptional-as-a-range добавляет поддержку итераций для boost :: optional, что может приблизить его. - person bradgonesurfing; 08.07.2010

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

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

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

person Community    schedule 08.07.2010
comment
Интересный момент. Вероятно, не случайно, что обработка ошибок, которую я видел в шаблонах, обычно осуществляется с помощью исключений. - person Steven Sudit; 08.07.2010
comment
stackoverflow.com/questions/3157098/ - person JUST MY correct OPINION; 08.07.2010
comment
Я отметил это, потому что это вводит в заблуждение. Исключения - не единственный выход. boost :: optional может отделить значение от ошибки. Вы можете думать о optional как о контейнере с нулем или одним элементом, который, по вашему мнению, вы должны иметь возможность перебирать. На самом деле я также отметил вышеупомянутый комментарий для его монады Maybe, которая, по сути, является просто дополнительным усилением плюс немного дополнительных конфет. У меня есть доказательство реализации этой идеи здесь. xtargets.heroku.com/2010/06/03 / using-boostoptional-as-a-range - person bradgonesurfing; 08.07.2010
comment
@bradgonesurfing Ну, вы также можете вернуть пару, например. Но я думаю, что если действительно есть ошибка, а не статус, то лучше всего будет исключение. И обычно SO не следует голосовать против из-за простого технического несогласия, когда неочевидно, что какая-либо из сторон права. - person ; 08.07.2010
comment
Извините за голосование "против". Я здесь новенький и только что получил 100 баллов, которые можно проголосовать против. Я буду избавлен от новой мощности;) На самом деле я где-то видел комбинацию техники boost :: optional и исключения в одном пакете. Это было очень круто, но сейчас я не могу этого вспомнить. Базовый класс был подобен дополнительному ускорению. Однако, если возвращаемое значение разрушается до того, как оно проверено, возникает исключение. Если он установлен, исключение не генерируется даже в случае ошибки. Это дает вам возможность выбора в едином API. - person bradgonesurfing; 09.07.2010
comment
@bradgonesurfing, я сделал нечто подобное еще в 2002 году. Я хотел написать в блоге об этом опыте, но я небрежно настроил свой блог. Мне было бы любопытно увидеть пакет, о котором вы говорите. - person Mark Ransom; 09.07.2010
comment
Я не могу его найти, хотя искал. Какой бы хорошей ни была идея, у нее есть один роковой недостаток. Выбрасывать исключение в деструкторе - это плохой тон. Если этот деструктор вызывается в рамках другого исключения, C ++ автоматически завершает свою работу. Это прекрасный пример того, как C ++ полностью нарушен в отношении RAII. В теории это хорошо, пока вы не обнаружите, что пишете более сложные деструкторы и не можете понять, бросают они или нет. - person bradgonesurfing; 09.07.2010
comment
@bradgonesurfing, я решил эту проблему, используя расширение Microsoft, которое вы могли вызвать, чтобы определить, находились ли вы уже в процессе обработки исключений. - person Mark Ransom; 10.07.2010
comment
@Mark: для этого не нужно расширение Microsoft; std::uncaught_exception() (от <exception>) сообщит вам, генерируется ли в данный момент исключение. - person Mike Seymour; 12.07.2010
comment
@Mike Наверное, только возможность проверить, а не использовать. Даже Херб Саттер говорит нет. gotw.ca/gotw/047.htm - person DumbCoder; 13.07.2010

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

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

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

vs.:

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

or:

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

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

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

person Jerry Coffin    schedule 08.07.2010
comment
Это не только COM, но и весь Win32 API, который использует отдельные сообщения об ошибках и результатах. с парой исключений из правила (но без исключений C ++): HANDLE-возвращающие функции имеют INVALID_HANDLE_VALUE и IUknown::AddRef и IUnknown::Release не возвращают HRESULT. Есть и веская причина: эти API-интерфейсы вызываются потребителями, написанными на разных языках. - person Ben Voigt; 08.07.2010
comment
+1 за признание необходимости отделить обнаружение от обработки. - person Steven Sudit; 08.07.2010
comment
Вы правы в том, что COM возвращает HRESULT при каждом вызове, в то время как естественное возвращаемое значение - это выходной параметр, отмеченный в IDL, что приводит к нечитаемому коду C. Однако, когда вы используете COM-оболочку C ++, такую ​​как ATL, все это безобразие скрывается. Вместо этого он превращается в вызов, который имеет обычное возвращаемое значение и выдает исключения, содержащие HRESULT. - person Steven Sudit; 08.07.2010
comment
@Ben: Остальная часть Win32 гораздо менее надежна, чем вы предполагаете. Достаточно функций, возвращающих дескрипторы, чтобы это само по себе было огромным исключением. Есть также немало, которые возвращают NULL, чтобы указать на сбой, включая HeapAlloc, CreateWindow, CreateDC и CreateIC. RegisterClass и RegisterClassEx возвращают ATOM или 0, чтобы указать на сбой. RegisterWindowMessage возвращает идентификатор сообщения или 0 в случае ошибки. У этого списка нет конца... - person Jerry Coffin; 08.07.2010
comment
@ Стивен: да, вполне возможно (и почти необходимо) скрыть, насколько плох дизайн, но это не меняет того факта, что это плохой дизайн ... - person Jerry Coffin; 08.07.2010
comment
@ Джерри: Согласен. Во всяком случае, необходимость скрыть уродство просто демонстрирует, насколько это плохо. - person Steven Sudit; 08.07.2010
comment
@ Стивен: Совершенно верно. В некотором смысле уродство - это даже хорошо - это так плохо, что нет никаких сомнений в том, что его нужно скрывать. Если бы дизайн был менее ужасным, люди могли бы смириться с этим ... - person Jerry Coffin; 08.07.2010
comment
@Jerry: Я могу встречаться с самим собой, но люди действительно мирились с этим, по крайней мере, какое-то время. Но нам это не понравилось. И, как вы сказали, было настолько очевидно плохо, что MS предоставила ATL (после первого возни с MFC), так что все хорошо, что хорошо кончается. Я не думаю, что уродства на низком уровне действительно можно избежать; важно, правильно ли он технически и может ли он быть аккуратно упакован для сохранения правильности. С учетом моделей потоков, поддерживаемых COM, ошибка errno / GetLastError, локальная для потока, не могла бы быть жизнеспособной на уровне IDL. - person Steven Sudit; 08.07.2010
comment
@Steven: Нет, в COM эквивалент GetLastError не является локальной глобальной переменной потока, он является для каждого объекта (возможно, локальным для потока также и в некоторых схемах потоковой передачи, я не помню). См. ISupportErrorInfo интерфейс. Но даже это менее уродливо, чем структуры данных, используемые для реализации исключений C ++. Что ОЧЕНЬ хорошо, так как это должно быть правильно реализовано на разных языках, иначе вся модель рухнет. - person Ben Voigt; 08.07.2010
comment
@ Бен: Я думаю, вы меня неправильно поняли. POSIX использует errno, а Windows использует GetLastError, оба кода являются локальными кодами ошибок потока, но COM не может этого сделать из-за своей потоковой модели, поэтому он возвращает HRESULT при каждом вызове. Вы правы, что ISupportErrorInfo похожа на GetLastError в том смысле, что это дополнительный вызов, который мы можем сделать после ошибки, чтобы узнать больше, но на самом деле это не то же самое, потому что HRESULT - это намного больше, чем просто его старший бит. - person Steven Sudit; 08.07.2010

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

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

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

person bta    schedule 09.07.2010

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

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

person KeithB    schedule 08.07.2010

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

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

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

person Martin Beckett    schedule 08.07.2010
comment
strcmp не возвращает false при совпадении. Как и любая функция сравнения, она возвращает положительный, отрицательный или нулевой результат, отражающий, является ли первый элемент больше, меньше или равен второму элементу в заданном порядке. В этом нет ничего волшебного. Это просто здравый смысл. Ожидаете ли вы, что (a-b) (функция сравнения чисел a и b) вернет истину, когда они совпадают, и ложь в противном случае?!? - person R.. GitHub STOP HELPING ICE; 08.07.2010
comment
Ни одно из возможных возвращаемых значений strcmp не указывает на ошибки, так что это плохой пример как для магических значений, так и для использования исключений. Возможно, fgetc (который возвращает магическое значение EOF) будет лучшим примером. - person Ben Voigt; 08.07.2010
comment
@R strcmp (), возвращающая ‹, 0,›, - удобный прием, если вы реализуете функцию сортировки. Обратной стороной являются все ошибки, внесенные людьми, пишущими if (strcmp ()), да, это вина программистов за то, что они не запомнили stdlib, но имя не помогает. - person Martin Beckett; 08.07.2010
comment
Вот почему считается хорошим стилем быть явным, когда проверяемое значение не является логическим: if (strcmp()==0) (pointer!=NULL) (number>0). К сожалению, программисты, которые не читают определения функций в стандартной библиотеке, совпадают с теми, кто не читает рекомендаций о хорошем и плохом стиле ...;) - person Secure; 08.07.2010
comment
@Martin: Это разные вещи. Он говорит о возникновении ошибки, а не о функциях, когда вопрос является неотъемлемой частью работы функции. Вы правы, есть некоторые функции, в которых сбой не является исключительным обстоятельством, но они также не возвращают коды ошибок. Мы говорим об ошибках, которые не являются отказами. - person Puppy; 08.07.2010
comment
@Secure: вот почему C # и Java не конвертируют автоматически int в bool. :-) - person Steven Sudit; 08.07.2010
comment
Как бы то ни было, это не столько взлом, сколько пример того, как низкоуровневые конструкции проникли в C. На уровне языка ассемблера вы получаете такие инструкции, как CMP EAX,0x1234, которые выполняют вычитание и устанавливают LT, EQ и GT. flags соответствующим образом, чтобы вы могли следовать инструкциям ветвления. В FORTRAN есть даже трехсторонняя конструкция IF - person Steven Sudit; 08.07.2010

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

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

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

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

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

person R.. GitHub STOP HELPING ICE    schedule 08.07.2010
comment
Я не считаю это реалистичным или полезным. - person Steven Sudit; 08.07.2010
comment
Тогда подумайте еще раз. Для пункта 1 в любом случае это лучший дизайн, потому что он более гибкий. Что делать, если у вас уже открыт файл и больше нет его имени? (Или, может быть, его названия больше не существует, или это канал / сокет и т. Д.) Пункт 2 очень открыт для обсуждения. Это накладывает некоторые более жесткие ограничения на реализацию, но в то же время может дать действительно большой прирост производительности. Пункт 3 - это простой здравый смысл ... - person R.. GitHub STOP HELPING ICE; 08.07.2010
comment
Уверяю вас, что моя реакция была не рефлексивной. Пункт 1 запрещает утилиту, такую ​​как XML DOM, которая позволяет загружать файлы из имени файла. Пункт 2 - в лучшем случае нишевый ответ, мало применимый в целом. Пункт 3 не столько ложный, сколько неуместный. В конечном счете, это моя самая большая жалоба: вопрос в том, как сообщать об ошибках, которые будут произойти, а не в том, как уменьшить этот набор. Хотя я за сокращение количества ошибок, он не решает поставленный вопрос и не всегда оправдывает себя. - person Steven Sudit; 08.07.2010
comment
Я с самого начала обратился к тому, что вы не можете устранить все ошибки (и, следовательно, необходимость создания отчетов об ошибках). Когда набор возможных ошибок огромен, большинство программистов, использующих ваши функции, просто игнорируют ошибки или имеют один плохо продуманный комплексный обработчик ошибок. Если набор возможных ошибок очень ограничен, вызывающей стороне намного проще их правильно обработать. - person R.. GitHub STOP HELPING ICE; 08.07.2010
comment
Некоторые добавленные мысли. Для пункта 1 (имена файлов и открытые файлы), возможно, стоит поддерживать и то, и другое, а также предварительно загруженные буферы в памяти. В качестве примера напомню, что раньше было болезненно использовать встроенные шрифты, потому что FreeType требовал, чтобы файл открывался (таким образом, ад временного файла). Современные версии поддерживают использование шрифтов в памяти. Хорошим примером для пункта 2 является компилятор регулярных выражений, который строит конечный автомат. Вы можете ограничить размер структур FA постоянным кратным длине строки регулярного выражения и просто предварительно выделить это, избегая возможности ошибок распределения в середине синтаксического анализа. - person R.. GitHub STOP HELPING ICE; 08.07.2010
comment
Неважно, насколько велик набор, просто пустой ли он. Если вызывающий абонент не знает, что делать с конкретной ошибкой, его обязанность - войти в систему и выйти из строя. Исключения делают это автоматически. - person Steven Sudit; 08.07.2010
comment
Что касается пункта 1, если мы согласны с тем, что мы действительно хотим поддерживать имена файлов, то мы соглашаемся, что у нас могут быть ошибки «файл не найден» и тому подобное. Я согласен с тем, что, по крайней мере, в некоторых случаях, имеет смысл разрешить передачу либо в буфер в памяти, либо в какой-либо форме потокового ввода. Для пункта 2 это не запускается, потому что начальное выделение все еще может завершиться ошибкой, и вам могут потребоваться промежуточные выделения. - person Steven Sudit; 08.07.2010
comment
Мне уже надоело спорить. Поскольку этот вопрос касался C / C ++, меня интересует случай C, когда у вас нет исключений. Даже если у вас есть исключения, вызывающие абоненты, как правило, плохо их обрабатывают или не обрабатывают совсем, поэтому стоит уменьшить количество случаев ошибок. Кстати, для моего конкретного примера пункта 2 (компилятор регулярных выражений) вы можете легко привязать общий скомпилированный размер и необходимое рабочее пространство. Единственный случай сбоя - это сбой всего начального распределения, и в этом случае вызывающий абонент несет ответственность еще до того, как совершит вызов. Вы не поверите, такие случаи - норма, а не исключение. - person R.. GitHub STOP HELPING ICE; 09.07.2010
comment
В C, где исключения недоступны, у меня иногда есть функции, которые немедленно возвращаются, если ошибка произошла при последнем вызове аналогичной функции, но не была опрошена. Если время попытки установления связи истекает, повторные попытки установить связь с тем же портом без подтверждения ошибки будут немедленно прерваны. Таким образом, можно безопасно иметь несколько программ связи без проверки ошибок между ними, а потом проверять, все ли работает. Если что-то не удалось, можно не знать, что не удалось, но это может не волновать. - person supercat; 12.07.2010

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

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

person JUST MY correct OPINION    schedule 08.07.2010

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

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

Пример:

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();

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

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

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

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

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

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

person Steven Sudit    schedule 08.07.2010
comment
Этот подход очень плох, если вы уже не передаете структуру контекста, в которой может сохраняться ошибка! В противном случае для него требуются глобальные переменные, и он не является потокобезопасным, если вы не прибегаете к неприятным (медленным или непереносимым, или и тем, и другим) мерам для сохранения статуса ошибки для каждого потока. - person R.. GitHub STOP HELPING ICE; 08.07.2010
comment
POSIX традиционно идет по этому пути для многих своих функций и имеет только одну волшебную псевдопеременную errno, в которой хранятся коды ошибок и которая гарантированно будет потокобезопасной. Так что, если вам не нужно больше, это определенно вариант. - person Jens Gustedt; 08.07.2010
comment
Как бы то ни было, я не тот, кто проголосовал против. Я бы на самом деле счел это хорошим ответом, если бы он был пересмотрен, чтобы отразить, что глобальное состояние плохое, и рекомендовал сохранить это состояние в структуре контекста. - person R.. GitHub STOP HELPING ICE; 08.07.2010
comment
@R .: GetLastError - это имя реального метода Windows API, который возвращает код ошибки thread-local. Шаблон использования заключается в проверке кода ошибки при сбое сразу после вызова. Таким образом, быть глобальным - приемлемый компромисс. - person Steven Sudit; 08.07.2010
comment
@ Стивен Судит: интересно, было бы неплохо иметь ссылку. Может быть, в нем говорится что-то, что errno может разрешить макрос или функцию. Но удивил бы меня, если бы он сделал заявление о безопасности потоков. - person Jens Gustedt; 08.07.2010
comment
Основная проблема с этим подходом: кто очищает переменную ошибки и когда? В любом случае, вы уже используете возвращаемое значение, чтобы указать на успех, это явно вариант второго метода, представленного в вопросе. - person Ben Voigt; 08.07.2010
comment
@Ben: переменная изменяется после каждого вызова API. Я согласен с тем, что это вариант, но он важный (но, видимо, противоречивый). - person Steven Sudit; 08.07.2010
comment
Жалоба на версию, возвращающую информативный код ошибки, похоже, игнорирует возможность: if (ERROR_SUCCESS != (errcode = RegOpenKey(HKEY_CLASSES_ROOT, NULL, &key))) ... - person Ben Voigt; 08.07.2010
comment
@Ben: Функционально он идентичен второму блоку кода, но еще более загадочен, поскольку объединяет в предикате присваивание и тестирование. Я знаю, что такое кодирование часто считается приемлемым в C, но я не совсем его фанат. Я сделал что-то похожее, но более чистое на C ++, используя вспомогательный класс ComResult. Его конструктор принимает HRESULT и сохраняет его в TLV. Экземпляр можно неявно привести к bool для обнаружения ошибок. Конструктор по умолчанию извлекает значение из TLV. В нем также были методы DidFail, DidSuccess и ThrowOnFail. - person Steven Sudit; 08.07.2010
comment
-1 за продолжение защиты глобальных переменных / глобального состояния. Тот факт, что у Windows (и POSIX) есть способы поддерживать это внутренне потокобезопасным способом, не меняет того факта, что это очень плохая практика для разработки ваших собственных API, тем более что невозможно сделать такой поток локальным. состояние таким образом, чтобы он был быстрым и портативным. - person R.. GitHub STOP HELPING ICE; 08.07.2010
comment
@R .: Выбор за вами: вы можете отобрать у меня мнимые точки или предложить убедительный аргумент. Пока вам удалось только первое. Например, ваш последний аргумент имеет небольшую проблему в том, что он фактически неверен. В C # все, что вам нужно сделать, это прикрепить атрибут ThreadStatic к статическому полю, и все готово. Он такой же переносимый, как .NET, и производительность неплохая, хотя это вряд ли имеет значение, поскольку вы проверяете только после того, как был обнаружен сбой. В C / C ++ вы должны использовать pthreads pthread_getspecific, чтобы получить тот же эффект. - person Steven Sudit; 08.07.2010
comment
Я должен уточнить, что, хотя я предлагал pthreads для максимальной переносимости, на самом деле я бы, вероятно, не стал возиться с этим. Вместо этого я бы использовал #define, который соответствует __declspec(thread) в MSCPP и __thread в GCC. Опять же, скорость не является проблемой, хотя ожидаемая производительность на самом деле довольно хорошая. - person Steven Sudit; 08.07.2010
comment
Стивен сказал, что это не имеет значения, поскольку вы проверяете только после того, как обнаружена неисправность. Фактически, переменная TLS должна быть назначена как при неудаче, так и при успешном выполнении, поэтому вы также платите штраф за поиск TLS на быстром пути. - person Ben Voigt; 08.07.2010
comment
@Ben: Это хороший момент. Если бы мы точно знали, что застряли с медленной реализацией (pthreads!), Мы могли бы оптимизировать это, определив интерфейс так, чтобы эквивалент errno устанавливался только при неудаче, поэтому проверка его при успехе дает неопределенное значение (предположительно самая последняя ошибка). На практике я думаю, что VC / GCC обеспечивает достаточную переносимость с хорошей скоростью, поэтому нам не нужно делать такого рода вещи. - person Steven Sudit; 08.07.2010
comment
@R .: Буду признателен за отзыв о дополнении. - person Steven Sudit; 09.07.2010
comment
Наличие состояния ошибки поддержки класса может быть хорошим подходом, если класс по своей природе не будет потокобезопасным. Он действительно исключает возможность того, что класс когда-либо будет потокобезопасным, но это не может быть убийцей одного: может быть несколько небезопасных для потоков командных объектов, которые действуют на общую потокобезопасную базу данных. - person supercat; 10.07.2010
comment
Дополнение: в C поддержание состояния ошибки может быть полезно, если предусматривается, что попытка выполнить функцию для некоторого объекта (файла, устройства или чего-либо еще) будет преждевременно завершена, если произошла ошибка, которая еще не была опрошена. Это может избежать необходимости проверять на ошибки каждую операцию в основном коде; можно попытаться выполнить несколько операций, а затем использовать одну проверку кода ошибки, чтобы убедиться, что все они работают. - person supercat; 10.07.2010
comment
@supercat: Я не думаю, что кто-то ожидал бы, что этот гипотетический класс-оболочка RegistryKey будет потокобезопасным, так что это не проблема. Если он действительно должен быть потокобезопасным, тогда потребуется TLV, как обсуждалось выше при слишком большой длине. - person Steven Sudit; 10.07.2010
comment
@supercat: Я много думал о твоем сценарии раннего выхода, и мне кажется, он мне не нравится. Если мы хотим избежать явных проверок ошибок и принудительно остановить вызывающую программу, мы можем выбросить исключение. Сценарий раннего выхода потребует, чтобы объект перешел в состояние сбоя, которое может быть сброшено только путем извлечения кода ошибки, а это означает, что если мы заботимся только о том, что произошел сбой, но не о причинах, он никогда не сбрасывается. Чем больше я смотрю на это, тем больше я предпочитаю просто вернуть bool. - person Steven Sudit; 10.07.2010
comment
@ Стивен: Так лучше, но я думаю, что я в значительной степени абсолютист в отношении глобального состояния. Также этот вопрос касается C и C ++, а не C #. Если вы должны сделать глобальное состояние для каждого потока, pthread_getspecific - это правильный путь (в отличие от непереносимых хаков), но производительность может сильно различаться. Жертвовать большой производительностью ради красивой системы отчетов об ошибках (которая для многих, в том числе и для меня, совсем не красивая, но крайне уродливая) - не лучший выбор в моей книге. Если состояния нет, просто передайте дополнительный int *errcode аргумент и покончите с этим. - person R.. GitHub STOP HELPING ICE; 10.07.2010
comment
@supercat: если класс должен быть потокобезопасным в смысле обеспечения одновременного доступа для записи из нескольких потоков, вы все равно можете использовать подход локальных данных потока. Классу просто нужно сохранять собственное состояние каждого потока внутри объекта. - person R.. GitHub STOP HELPING ICE; 10.07.2010
comment
@R .: Спасибо за ваш вклад. Я явно не сторонник абсолютизма и не считаю оптимизацию для конкретного компилятора хакерством. Учитывая это, для меня проблема не в том, чтобы жертвовать большой производительностью, поэтому преимущества отделения того, действительно ли от причины, легко перевешивают недостатки, основанные на надежности, а не на привлекательности. Короче говоря, мы до сих пор не согласны, но теперь мы видим, где и почему. - person Steven Sudit; 10.07.2010
comment
@Steven Sudit: Случай раннего выхода полезен (по крайней мере, я его использую) в C, где исключения недоступны. - person supercat; 12.07.2010
comment
@Steven Sudit: Большая часть моего опыта работы с C связана со встроенными системами. Хотя я иногда использовал хаки с указателем стека для таких вещей, как совместная многозадачность (может быть ОЧЕНЬ удобна), пространство кода часто не хватает. Кодирование последовательности операций ввода-вывода с последующей проверкой ошибки более компактно в исходной и скомпилированной форме, чем проверка на наличие ошибки после каждой операции. - person supercat; 12.07.2010
comment
@supercat: Похоже, еще один случай правильного ответа в зависимости от специфики вопроса, например платформы. :-) - person Steven Sudit; 12.07.2010

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

person PeterK    schedule 08.07.2010

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

person pcent    schedule 08.07.2010

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

person Puppy    schedule 08.07.2010

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

person aitorkun    schedule 08.07.2010

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

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

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

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

person Praveen S    schedule 08.07.2010

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

person supercat    schedule 09.07.2010

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

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

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

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

person John    schedule 08.07.2010
comment
Ой, не волнуйся. Не глупо голосовать против разумного ответа на этом сайте ... это ожидаемо! - person John; 09.07.2010