C: Как вы моделируете «исключение»? - PullRequest
14 голосов
/ 02 августа 2009

Я родом из C #, но сейчас изучаю 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 ранее в стек, как вы узнаете, пуст ли стек или было ли возвращено правильное значение?

Ответы [ 12 ]

15 голосов
/ 02 августа 2009

Исключительное поведение в C достигается через setjmp / longjmp . Однако то, что вы действительно хотите здесь, это код ошибки. Если все значения потенциально могут быть возвращаемыми, то вы можете принять выходной параметр в качестве указателя и использовать его для возврата значения, например:

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.

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

10 голосов
/ 02 августа 2009

Вы можете построить систему исключений поверх 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 # или C ++. Основная проблема заключается в том, что только ваш код будет совместим с этой системой исключений. Поскольку не существует установленного стандарта для исключений в C, системные и сторонние библиотеки просто не будут оптимально взаимодействовать с вашей собственной системой исключений. Тем не менее, иногда это может оказаться полезным хаком.

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

7 голосов
/ 02 августа 2009

У вас есть несколько вариантов:

1) Значение магической ошибки. Не всегда достаточно хорошо, по той причине, которую вы описываете. Я полагаю, теоретически для этого случая вы могли бы вернуть NaN, но я не рекомендую его.

2) Определите, что всплывающее окно недопустимо, когда стек пуст. Тогда ваш код либо просто предполагает, что он не пустой (и становится неопределенным, если он есть), либо утверждает.

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 голосов
/ 03 августа 2009

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

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

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

Это происходит ВСЕ время, но в C ++, чтобы избежать таких двусмысленностей, обычно просто возвращается

std::pair<something,bool>

, где bool является индикатором успеха - посмотрите на некоторые из:

std::set<...>::insert
std::map<...>::insert

Либо добавьте к интерфейсу double* и верните код возврата (n UNOVERLOADED!), Скажем, enum, указывающий на успех.

Конечно, не нужно возвращать размер в структуре popRC. Это могло быть

enum{FAIL,SUCCESS};

Но так как размер может послужить полезной подсказкой для pop'er, вы также можете использовать его.

Кстати, я от всей души согласен, что интерфейс структуры стека должен иметь

int empty(struct stack* pS){
    return (pS->size == 0) ? 1 : 0;
}
4 голосов
/ 02 августа 2009

Один из подходов состоит в том, чтобы указать, что pop () имеет неопределенное поведение, если стек пуст. Затем вам нужно предоставить функцию is_empty (), которая может быть вызвана для проверки стека.

Другой подход заключается в использовании C ++, у которого есть исключения: -)

3 голосов
/ 02 августа 2009

В таких случаях вы обычно делаете один из

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

например. вы либо делаете

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;

   }
}
2 голосов
/ 02 августа 2009

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

Механизмы, доступные в C:

  • Нелокальные gotos с setjmp / longjmp
  • Сигналы

Однако ни одна из них не имеет семантики, удаленно напоминающей исключения C # (или C ++).

1 голос
/ 20 октября 2010

То, что еще никто не упомянул, хотя и довольно уродливо:

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");
}
1 голос
/ 03 августа 2009

1) Вы возвращаете значение флага, чтобы показать, что оно не выполнено, или используете синтаксис TryGet, где возвращаемое значение является логическим для успеха, в то время как значение передается через выходной параметр.

2) Если это в Windows, существует исключительная ситуация на чистом языке C, называемая обработкой исключений Structed Exception, с использованием синтаксиса, подобного "_try". Я упоминаю об этом, но я не рекомендую это для этого случая.

1 голос
/ 02 августа 2009

Вы можете вернуть указатель на удвоение:

  • не NULL -> действительный
  • NULL -> недействительно
...