C: Как вы имитируете "исключение"?
Я родом из С#, но сейчас изучаю C. В С#, когда вы хотите сообщить, что произошла ошибка, вы генерируете исключение. Но что вы делаете в C?
Скажем, например, у вас есть стек с функциями push
и pop
. Каков наилучший способ оповестить, что стек пуст во время pop
? Что вы возвращаете из этой функции?
double pop(void)
{
if(sp > 0)
return val[--sp];
else {
printf("error: stack empty\n");
return 0.0;
}
}
Пример K & R на странице 77 (код выше) возвращает 0.0
. Но что, если пользователь нажал 0.0
раньше в стеке, как вы знаете, пустой ли стек или вернётся ли правильное значение?
Ответы
Ответ 1
Исключительное поведение в C выполняется с помощью setjmp/longjmp. Однако, что вы действительно хотите здесь, это код ошибки. Если все значения потенциально возвращаются, вы можете захотеть взять указатель out-parameter в качестве указателя и использовать его для возврата значения так:
int pop(double* outval)
{
if(outval == 0) return -1;
if(sp > 0)
*outval = val[--sp];
else {
printf("error: stack empty\n");
return -1;
}
return 0;
}
Не идеальны, очевидно, но таковы ограничения C.
Кроме того, если вы идете по этой дороге, вы можете определить символические константы для своих кодов ошибок (или использовать некоторые из стандартные), так что пользователь может различать "пустой стек" и "вы дали мне нулевой указатель, тупицу".
Ответ 2
Вы можете создать систему исключений поверх longjmp/setjmp: Исключения в C с Longjmp и Setjmp. Это действительно работает очень хорошо, и статья хорошо читается. Вот как выглядел бы ваш код, если бы вы использовали систему исключений из связанной статьи:
TRY {
...
THROW(MY_EXCEPTION);
/* Unreachable */
} CATCH(MY_EXCEPTION) {
...
} CATCH(OTHER_EXCEPTION) {
...
} FINALLY {
...
}
Удивительно, что вы можете сделать с помощью маленьких макросов, не так ли? Не менее удивительно, как сложно понять, что происходит, если вы еще не знаете, что делают макросы.
longjmp/setjmp переносимы: C89, C99 и POSIX.1-2001 указывают setjmp()
.
Обратите внимание, однако, что исключения, реализованные таким образом, будут по-прежнему иметь некоторые ограничения по сравнению с "реальными" исключениями на С# или С++. Основная проблема заключается в том, что только ваш код будет совместим с этой системой исключений. Поскольку не существует стандартного для исключений в C, системные и сторонние библиотеки просто не будут взаимодействовать оптимально с вашей доморощенной системой исключений. Тем не менее, иногда это может оказаться полезным взломом.
Я не рекомендую использовать это в серьезном коде, с которым должны работать программисты, кроме вас. Слишком легко стрелять в ногу с этим, если вы точно не знаете, что происходит. Threading, управление ресурсами и обработка сигналов - это проблемные области, с которыми будут сталкиваться не игрушечные программы, если вы попытаетесь использовать "исключения" longjmp.
Ответ 3
У вас есть несколько вариантов:
1) Значение магической ошибки. Не всегда достаточно хорошо, по причине, которую вы описываете. Я думаю, в теории для этого случая вы могли бы вернуть NaN, но я не рекомендую его.
2) Определите, что он недействителен для pop, когда стек пуст. Тогда ваш код либо просто предполагает, что он не пуст (и идет undefined, если он есть), либо утверждает.
3) Измените подпись функции, чтобы указать успешность или неудачу:
int pop(double *dptr)
{
if(sp > 0) {
*dptr = val[--sp];
return 0;
} else {
return 1;
}
}
Подтвердите его как "Если он успешный, он возвращает 0 и записывает значение в адрес, на который указывает dptr. При ошибке возвращает ненулевое значение."
При желании вы можете использовать возвращаемое значение или errno
, чтобы указать причину сбоя, хотя для этого конкретного примера существует только одна причина.
4) Передайте объект "исключение" в каждую функцию указателем и напишите ему значение при ошибке. Затем вызывающий абонент проверяет это или нет в соответствии с тем, как они используют возвращаемое значение. Это очень похоже на использование "errno", но без его значения по ширине.
5) Как говорили другие, выполняйте исключения с помощью setjmp/longjmp. Это выполнимо, но требует либо передачи дополнительного параметра во всем мире (цель longjmp для выполнения при сбое), либо скрытие его в глобальных переменных. Это также приводит к тому, что типичный ресурс C-стиля обрабатывает кошмар, потому что вы не можете называть ничего, что могло бы выпрыгнуть за пределы вашего уровня стека, если вы держите ресурс, за который вы отвечаете за освобождение.
Ответ 4
Один из подходов - указать, что pop() имеет поведение undefined, если стек пуст. Затем вам необходимо предоставить функцию is_empty(), которая может быть вызвана для проверки стека.
Другой подход - использовать С++, который имеет исключения: -)
Ответ 5
Это на самом деле прекрасный пример зла, пытающегося перегрузить возвращаемый тип магическими значениями и просто сомнительный дизайн интерфейса.
Одним из решений, которое я мог бы использовать для устранения неоднозначности (и, следовательно, необходимости "исключения, такого как поведение" ) в этом примере, является определение правильного типа возврата:
struct stack{
double* pData;
uint32 size;
};
struct popRC{
double value;
uint32 size_before_pop;
};
popRC pop(struct stack* pS){
popRC rc;
rc.size=pS->size;
if(rc.size){
--pS->size;
rc.value=pS->pData[pS->size];
}
return rc;
}
Использование:
popRC rc = pop(&stack);
if(rc.size_before_pop!=0){
....use rc.value
Это происходит ВСЕ ВРЕМЯ, но в С++, чтобы избежать таких неоднозначностей, обычно обычно возвращается
std::pair<something,bool>
где bool - индикатор успеха - посмотрите на некоторые из:
std::set<...>::insert
std::map<...>::insert
Альтернативно добавьте double*
к интерфейсу и верните код возврата (n UNOVERLOADED!), скажем, перечисление, указывающее успех.
Конечно, не нужно было возвращать размер в struct popRC
. Это могло быть
enum{FAIL,SUCCESS};
Но так как размер может служить полезным подсказкой для pop'er, вы можете его использовать.
Кстати, я искренне согласен с тем, что интерфейс стека структуры должен иметь
int empty(struct stack* pS){
return (pS->size == 0) ? 1 : 0;
}
Ответ 6
В таких случаях, как правило, вы делаете один из
- Оставьте его вызывающему. например до вызывающего абонента, чтобы узнать, безопасно ли его поп() (например, вызвать функцию stack- > is_empty() перед тем, как вытащить стек), и если вызывающий абонент запутался, это его вина и удача.
- Сигнал ошибки через параметр out или возвращаемое значение.
например. вы либо делаете
double pop(int *error)
{
if(sp > 0) {
return val[--sp];
*error = 0;
} else {
*error = 1;
printf("error: stack empty\n");
return 0.0;
}
}
или
int pop(double *d)
{
if(sp > 0) {
*d = val[--sp];
return 0;
} else {
return 1;
}
}
Ответ 7
Не существует эквивалента исключений в прямой C. Вам нужно создать свою сигнатуру функции, чтобы возвращать информацию об ошибке, если это то, что вы хотите.
Механизмы, доступные в C:
- Нелокальные gotos с setjmp/longjmp
- Сигналы
Однако ни одна из них не имеет семантики, удаленно напоминающей исключения С# (или С++).
Ответ 8
вы можете вернуть указатель в double:
- не-NULL → действительный
- NULL → недействительный
Ответ 9
1) Вы возвращаете значение флага, чтобы показать его сбой, или вы используете синтаксис TryGet, где возврат является логическим для успеха, в то время как значение передается через выходной параметр.
2) Если это под Windows, есть форма исключений на уровне ОС, чистая форма C, называемая Structed Exception Handling, с использованием синтаксиса типа "_try". Я упоминаю об этом, но я не рекомендую его для этого случая.
Ответ 10
Что-то, о чем никто еще не упомянул, довольно уродливо:
int ok=0;
do
{
/* Do stuff here */
/* If there is an error */
break;
/* If we got to the end without an error */
ok=1;
} while(0);
if (ok == 0)
{
printf("Fail.\n");
}
else
{
printf("Ok.\n");
}
Ответ 11
Здесь уже есть некоторые хорошие ответы, просто хотелось бы упомянуть, что что-то близкое к "исключению", может быть сделано с использованием макроса, как это делалось в awesome MinUnit (это возвращает только "исключение" к функции вызывающего абонента).
Ответ 12
setjmp
, longjmp
и макросы. Это было сделано несколько раз - самая старая реализация, о которой я знаю, - это Эрик Робертс и Марк ВандерВорде, но тот, который я использую в настоящее время, является частью Дэйва Хансона C Интерфейсов и реализаций и не имеет доступа к Принстону.