Попробуйте операторы catch на C

103

Сегодня я думал о блоках try / catch, существующих в других языках. Некоторое время гуглил, но безрезультатно. Насколько я знаю, в C. нет такой вещи, как try / catch. Однако есть ли способ их «смоделировать»?
Конечно, есть assert и другие уловки, но ничего вроде try / catch, которые также перехватывают возникшее исключение. Спасибо

Андрей
источник
4
Механизмы, подобные исключениям, обычно не будут полезны без механизма автоматического освобождения ресурсов при раскручивании стека. C ++ использует RAII; Java, C #, Python и т. Д. Используют сборщики мусора. (И обратите внимание, что сборщики мусора освобождают только память. Чтобы автоматически освобождать другие типы ресурсов, они также добавляют такие вещи, как финализаторы или менеджеры контекста ...)
jamesdlin
@jamesdlin, почему мы не могли сделать RAII с C?
Pacerier
1
@Pacerier RAII требует автоматического вызова функций при уничтожении объектов (т. Е. Деструкторов). Как вы предлагаете сделать это на C?
jamesdlin

Ответы:

92

Сам C не поддерживает исключения , но вы можете моделировать их в определенной степени с setjmpи longjmpвызовов.

static jmp_buf s_jumpBuffer;

void Example() { 
  if (setjmp(s_jumpBuffer)) {
    // The longjmp was executed and returned control here
    printf("Exception happened here\n");
  } else {
    // Normal code execution starts here
    Test();
  }
}

void Test() {
  // Rough equivalent of `throw`
  longjmp(s_jumpBuffer, 42);
}

На этом веб-сайте есть хороший учебник о том, как моделировать исключения с помощью setjmpиlongjmp

ДжаредПар
источник
1
классное решение! это решение крест? Это сработало для меня на MSVC2012, но не на компиляторе MacOSX Clang.
Mannysz 05
1
Подскажите: я думал, что предложения try catch позволяют перехватывать исключения (например, деление на ноль). Эта функция, похоже, позволяет вам перехватывать исключения, которые вы выбрасываете сами. Реальные исключения не возникают при вызове longjmp, верно? Если я использую этот код для чего-то подобного, try{ x = 7 / 0; } catch(divideByZeroException) {print('divided by zero')}; он не сработает, верно?
Сэм
Деление на ноль - это даже не исключение в C ++, чтобы справиться с этим, вам нужно либо проверить, не равен ли делитель нулю и обработать его, либо обработать SIGFPE, который генерируется при запуске формулы деления на ноль.
Джеймс
25

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

Алок Сохранить
источник
3
@JensGustedt Это именно то, для чего в настоящее время очень часто используется goto, и пример, где это имеет смысл (setjmp / ljmp - лучшая альтернатива, но метка + goto обычно используется чаще).
Томаш Пружина
1
@AoeAoe, наверное goto, больше используется для обработки ошибок, ну и что? Вопрос не в обработке ошибок как таковой, а явно в эквивалентах try / catch. gotoне является эквивалентом try / catch, поскольку он ограничен той же функцией.
Йенс Густедт
@JensGustedt Я как бы реагировал на ненависть / страх перед goto и людьми, которые его используют (мои учителя рассказывали мне страшные истории об использовании goto в университете). [OT] Единственное, что действительно, очень рискованно и «облачно» в goto - это «goto backwards», но я видел это в Linux VFS (git blame guy поклялся, что это критично для производительности).
Томаш Пружина
См. Источники systemctl для законного использования в gotoкачестве механизма try / catch, используемого в современном, широко признанном, рецензируемом источнике. Найдите gotoэквивалент «бросить» и эквивалент finish«поймать».
Стюарт
14

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

Мы можем использовать злоупотребляйте препроцессор и локальный переменные стека , чтобы дать использовать ограниченный вариант C ++ Try / броска / улова.

Версия 1 (выбрасывает локальный прицел)

#include <stdbool.h>

#define try bool __HadError=false;
#define catch(x) ExitJmp:if(__HadError)
#define throw(x) __HadError=true;goto ExitJmp;

Версия 1 - это только локальный выброс (не может покинуть область действия функции). Он действительно зависит от способности C99 объявлять переменные в коде (он должен работать в C89, если попытка является первым делом в функции).

Эта функция просто создает локальную переменную, чтобы она знала, произошла ли ошибка, и использует goto для перехода к блоку catch.

Например:

#include <stdio.h>
#include <stdbool.h>

#define try bool __HadError=false;
#define catch(x) ExitJmp:if(__HadError)
#define throw(x) __HadError=true;goto ExitJmp;

int main(void)
{
    try
    {
        printf("One\n");
        throw();
        printf("Two\n");
    }
    catch(...)
    {
        printf("Error\n");
    }
    return 0;
}

Это работает примерно так:

int main(void)
{
    bool HadError=false;
    {
        printf("One\n");
        HadError=true;
        goto ExitJmp;
        printf("Two\n");
    }
ExitJmp:
    if(HadError)
    {
        printf("Error\n");
    }
    return 0;
}

Версия 2 (скачок прицела)

#include <stdbool.h>
#include <setjmp.h>

jmp_buf *g__ActiveBuf;

#define try jmp_buf __LocalJmpBuff;jmp_buf *__OldActiveBuf=g__ActiveBuf;bool __WasThrown=false;g__ActiveBuf=&__LocalJmpBuff;if(setjmp(__LocalJmpBuff)){__WasThrown=true;}else
#define catch(x) g__ActiveBuf=__OldActiveBuf;if(__WasThrown)
#define throw(x) longjmp(*g__ActiveBuf,1);

Версия 2 намного сложнее, но в основном работает так же. Он использует длинный переход от текущей функции к блоку try. Затем блок try использует if / else, чтобы пропустить блок кода к блоку catch, который проверяет локальную переменную, чтобы увидеть, должна ли она перехватывать.

Пример снова расширился:

jmp_buf *g_ActiveBuf;

int main(void)
{
    jmp_buf LocalJmpBuff;
    jmp_buf *OldActiveBuf=g_ActiveBuf;
    bool WasThrown=false;
    g_ActiveBuf=&LocalJmpBuff;

    if(setjmp(LocalJmpBuff))
    {
        WasThrown=true;
    }
    else
    {
        printf("One\n");
        longjmp(*g_ActiveBuf,1);
        printf("Two\n");
    }
    g_ActiveBuf=OldActiveBuf;
    if(WasThrown)
    {
        printf("Error\n");
    }
    return 0;
}

При этом используется глобальный указатель, поэтому longjmp () знает, какая попытка была выполнена последней. Мы используем злоупотребляя стек так дочерние функции могут также попробовать / поймать блок.

Использование этого кода имеет ряд недостатков (но это забавное умственное упражнение):

  • Он не будет освобождать выделенную память, так как не вызываются деконструкторы.
  • У вас не может быть более 1 попытки / улова в прицеле (без вложенности)
  • На самом деле вы не можете генерировать исключения или другие данные, как в C ++
  • Не потокобезопасный вообще
  • Вы настраиваете других программистов на сбой, потому что они, скорее всего, не заметят взлома и попытаются использовать их как блоки try / catch C ++.
Пол Хатчинсон
источник
хорошие альтернативные решения.
HaseeB Mir
версия 1 - хорошая идея, но эту переменную __HadError нужно будет сбросить или ограничить. В противном случае вы не сможете использовать более одного try-catch в одном блоке. Может быть, использовать глобальную функцию вроде bool __ErrorCheck(bool &e){bool _e = e;e=false;return _e;}. Но локальная переменная также будет переопределена, поэтому ситуация немного выйдет из-под контроля.
flamewave000
Да, это ограничено одним try-catch в той же функции. Однако более серьезной проблемой, чем переменная, является метка, поскольку у вас не может быть повторяющихся меток в одной и той же функции.
Пол Хатчинсон,
10

В C99 вы можете использовать setjmp/ longjmpдля нелокального потока управления.

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

Керрек С.Б.
источник
5

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

  1. Вложение блоков try / catch. Использование одной глобальной переменной для вашегоjmp_buf сделает их неработоспособными.
  2. Поток. Единственная глобальная переменная для вас jmp_bufвызовет в этой ситуации все виды боли.

Решением для них является поддержка локального стека потока jmp_buf который будет обновляться по мере вашего продвижения. (Я думаю, это то, что lua использует для внутренних целей).

Итак, вместо этого (из потрясающего ответа JaredPar)

static jmp_buf s_jumpBuffer;

void Example() { 
  if (setjmp(s_jumpBuffer)) {
    // The longjmp was executed and returned control here
    printf("Exception happened\n");
  } else {
    // Normal code execution starts here
    Test();
  }
}

void Test() {
  // Rough equivalent of `throw`
  longjump(s_jumpBuffer, 42);
}

Вы бы использовали что-то вроде:

#define MAX_EXCEPTION_DEPTH 10;
struct exception_state {
  jmp_buf s_jumpBuffer[MAX_EXCEPTION_DEPTH];
  int current_depth;
};

int try_point(struct exception_state * state) {
  if(current_depth==MAX_EXCEPTION_DEPTH) {
     abort();
  }
  int ok = setjmp(state->jumpBuffer[state->current_depth]);
  if(ok) {
    state->current_depth++;
  } else {
    //We've had an exception update the stack.
    state->current_depth--;
  }
  return ok;
}

void throw_exception(struct exception_state * state) {
  longjump(state->current_depth-1,1);
}

void catch_point(struct exception_state * state) {
    state->current_depth--;
}

void end_try_point(struct exception_state * state) {
    state->current_depth--;
}

__thread struct exception_state g_exception_state; 

void Example() { 
  if (try_point(&g_exception_state)) {
    catch_point(&g_exception_state);
    printf("Exception happened\n");
  } else {
    // Normal code execution starts here
    Test();
    end_try_point(&g_exception_state);
  }
}

void Test() {
  // Rough equivalent of `throw`
  throw_exception(g_exception_state);
}

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

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: приведенный выше код был написан без какого-либо тестирования. Это чисто для того, чтобы вы получили представление о том, как структурировать вещи. Разные системы и разные компиляторы должны будут по-разному реализовать локальное хранилище потока. Код, вероятно, содержит как ошибки компиляции, так и логические ошибки, поэтому, пока вы можете использовать его по своему усмотрению, ПРОВЕРЬТЕ его перед использованием;)

Майкл Андерсон
источник
4

Быстрый поиск в Google дает беспорядочные решения, такие как этот которые используют setjmp / longjmp, как упоминали другие. Нет ничего более простого и элегантного, чем try / catch в C ++ / Java. Я сам неравнодушен к обработке исключений в Аде.

Проверьте все с помощью операторов if :)

Джеймс Адам
источник
4

Это можно сделать с помощью setjmp/longjmpC. P99 имеет довольно удобный набор инструментов для этого, который также совместим с новой моделью резьбы C11.

Йенс Густедт
источник
2

Это еще один способ обработки ошибок в C, который более эффективен, чем использование setjmp / longjmp. К сожалению, это не будет работать с MSVC, но если можно использовать только GCC / Clang, вы можете рассмотреть его. В частности, он использует расширение «метка как значение», которое позволяет вам взять адрес метки, сохранить его в значении и безоговорочно перейти к нему. Я представлю это на примере:

GameEngine *CreateGameEngine(GameEngineParams const *params)
{
    /* Declare an error handler variable. This will hold the address
       to jump to if an error occurs to cleanup pending resources.
       Initialize it to the err label which simply returns an
       error value (NULL in this example). The && operator resolves to
       the address of the label err */
    void *eh = &&err;

    /* Try the allocation */
    GameEngine *engine = malloc(sizeof *engine);
    if (!engine)
        goto *eh; /* this is essentially your "throw" */

    /* Now make sure that if we throw from this point on, the memory
       gets deallocated. As a convention you could name the label "undo_"
       followed by the operation to rollback. */
    eh = &&undo_malloc;

    /* Now carry on with the initialization. */
    engine->window = OpenWindow(...);
    if (!engine->window)
        goto *eh;   /* The neat trick about using approach is that you don't
                       need to remember what "undo" label to go to in code.
                       Simply go to *eh. */

    eh = &&undo_window_open;

    /* etc */

    /* Everything went well, just return the device. */
    return device;

    /* After the return, insert your cleanup code in reverse order. */
undo_window_open: CloseWindow(engine->window);
undo_malloc: free(engine);
err: return NULL;
}

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

/* Put at the beginning of a function that may fail. */
#define declthrows void *_eh = &&err

/* Cleans up resources and returns error result. */
#define throw goto *_eh

/* Sets a new undo checkpoint. */
#define undo(label) _eh = &&undo_##label

/* Throws if [condition] evaluates to false. */
#define check(condition) if (!(condition)) throw

/* Throws if [condition] evaluates to false. Then sets a new undo checkpoint. */
#define checkpoint(label, condition) { check(condition); undo(label); }

Тогда пример становится

GameEngine *CreateGameEngine(GameEngineParams const *params)
{
    declthrows;

    /* Try the allocation */
    GameEngine *engine = malloc(sizeof *engine);
    checkpoint(malloc, engine);

    /* Now carry on with the initialization. */
    engine->window = OpenWindow(...);
    checkpoint(window_open, engine->window);

    /* etc */

    /* Everything went well, just return the device. */
    return device;

    /* After the return, insert your cleanup code in reverse order. */
undo_window_open: CloseWindow(engine->window);
undo_malloc: free(engine);
err: return NULL;
}
Keebus
источник
2

Предупреждение: это не очень хорошо, но работает.

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    unsigned int  id;
    char         *name;
    char         *msg;
} error;

#define _printerr(e, s, ...) fprintf(stderr, "\033[1m\033[37m" "%s:%d: " "\033[1m\033[31m" e ":" "\033[1m\033[37m" " ‘%s_error’ " "\033[0m" s "\n", __FILE__, __LINE__, (*__err)->name, ##__VA_ARGS__)
#define printerr(s, ...) _printerr("error", s, ##__VA_ARGS__)
#define printuncaughterr() _printerr("uncaught error", "%s", (*__err)->msg)

#define _errordef(n, _id) \
error* new_##n##_error_msg(char* msg) { \
    error* self = malloc(sizeof(error)); \
    self->id = _id; \
    self->name = #n; \
    self->msg = msg; \
    return self; \
} \
error* new_##n##_error() { return new_##n##_error_msg(""); }

#define errordef(n) _errordef(n, __COUNTER__ +1)

#define try(try_block, err, err_name, catch_block) { \
    error * err_name = NULL; \
    error ** __err = & err_name; \
    void __try_fn() try_block \
    __try_fn(); \
    void __catch_fn() { \
        if (err_name == NULL) return; \
        unsigned int __##err_name##_id = new_##err##_error()->id; \
        if (__##err_name##_id != 0 && __##err_name##_id != err_name->id) \
            printuncaughterr(); \
        else if (__##err_name##_id != 0 || __##err_name##_id != err_name->id) \
            catch_block \
    } \
    __catch_fn(); \
}

#define throw(e) { *__err = e; return; }

_errordef(any, 0)

Использование:

errordef(my_err1)
errordef(my_err2)

try ({
    printf("Helloo\n");
    throw(new_my_err1_error_msg("hiiiii!"));
    printf("This will not be printed!\n");
}, /*catch*/ any, e, {
    printf("My lovely error: %s %s\n", e->name, e->msg);
})

printf("\n");

try ({
    printf("Helloo\n");
    throw(new_my_err2_error_msg("my msg!"));
    printf("This will not be printed!\n");
}, /*catch*/ my_err2, e, {
    printerr("%s", e->msg);
})

printf("\n");

try ({
    printf("Helloo\n");
    throw(new_my_err1_error());
    printf("This will not be printed!\n");
}, /*catch*/ my_err2, e, {
    printf("Catch %s if you can!\n", e->name);
})

Вывод:

Helloo
My lovely error: my_err1 hiiiii!

Helloo
/home/naheel/Desktop/aa.c:28: error: my_err2_error my msg!

Helloo
/home/naheel/Desktop/aa.c:38: uncaught error: my_err1_error 

Имейте в виду, что здесь используются вложенные функции и __COUNTER__. Вы будете в безопасности, если используете gcc.

Naheel
источник
1

Redis использует goto для имитации try / catch, IMHO это очень чисто и элегантно:

/* Save the DB on disk. Return REDIS_ERR on error, REDIS_OK on success. */
int rdbSave(char *filename) {
    char tmpfile[256];
    FILE *fp;
    rio rdb;
    int error = 0;

    snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
    fp = fopen(tmpfile,"w");
    if (!fp) {
        redisLog(REDIS_WARNING, "Failed opening .rdb for saving: %s",
            strerror(errno));
        return REDIS_ERR;
    }

    rioInitWithFile(&rdb,fp);
    if (rdbSaveRio(&rdb,&error) == REDIS_ERR) {
        errno = error;
        goto werr;
    }

    /* Make sure data will not remain on the OS's output buffers */
    if (fflush(fp) == EOF) goto werr;
    if (fsync(fileno(fp)) == -1) goto werr;
    if (fclose(fp) == EOF) goto werr;

    /* Use RENAME to make sure the DB file is changed atomically only
     * if the generate DB file is ok. */
    if (rename(tmpfile,filename) == -1) {
        redisLog(REDIS_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
        unlink(tmpfile);
        return REDIS_ERR;
    }
    redisLog(REDIS_NOTICE,"DB saved on disk");
    server.dirty = 0;
    server.lastsave = time(NULL);
    server.lastbgsave_status = REDIS_OK;
    return REDIS_OK;

werr:
    fclose(fp);
    unlink(tmpfile);
    redisLog(REDIS_WARNING,"Write error saving DB on disk: %s", strerror(errno));
    return REDIS_ERR;
}
Форрест Йе
источник
Код не работает. errnoдолжен использоваться только сразу после неудачного системного вызова, а не тремя вызовами позже.
ceven 04
Этот код дублирует логику обработки ошибок в нескольких местах и ​​может делать неправильные вещи, например многократно вызывать fclose (fp). Было бы намного лучше использовать несколько меток и закодировать то, что еще нужно исправить, с помощью этих меток (а не только одну для всех ошибок), а затем перейти к правильному месту обработки ошибок в зависимости от того, где в коде возникает ошибка.
jschultz410
1

В C вы можете «имитировать» исключения вместе с автоматическим «восстановлением объекта» посредством ручного использования if + goto для явной обработки ошибок.

Я часто пишу C-код, подобный следующему (сводится к тому, чтобы выделить обработку ошибок):

#include <assert.h>

typedef int errcode;

errcode init_or_fail( foo *f, goo *g, poo *p, loo *l )
{
    errcode ret = 0;

    if ( ( ret = foo_init( f ) ) )
        goto FAIL;

    if ( ( ret = goo_init( g ) ) )
        goto FAIL_F;

    if ( ( ret = poo_init( p ) ) )
        goto FAIL_G;

    if ( ( ret = loo_init( l ) ) )
        goto FAIL_P;

    assert( 0 == ret );
    goto END;

    /* error handling and return */

    /* Note that we finalize in opposite order of initialization because we are unwinding a *STACK* of initialized objects */

FAIL_P:
    poo_fini( p );

FAIL_G:
    goo_fini( g );

FAIL_F:
    foo_fini( f );

FAIL:
    assert( 0 != ret );

END:
    return ret;        
}

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

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

#include <assert.h>
#include <stdio.h>
#include <string.h>

#define TRY( X, LABEL ) do { if ( ( X ) ) { fprintf( stderr, "%s:%d: Statement '" #X "' failed! %d, %s\n", __FILE__, __LINE__, ret, strerror( ret ) ); goto LABEL; } while ( 0 )

typedef int errcode;

errcode init_or_fail( foo *f, goo *g, poo *p, loo *l )
{
    errcode ret = 0;

    TRY( ret = foo_init( f ), FAIL );
    TRY( ret = goo_init( g ), FAIL_F );
    TRY( ret = poo_init( p ), FAIL_G );
    TRY( ret = loo_init( l ), FAIL_P );

    assert( 0 == ret );
    goto END;

    /* error handling and return */

FAIL_P:
    poo_fini( p );

FAIL_G:
    goo_fini( g );

FAIL_F:
    foo_fini( f );

FAIL:
    assert( 0 != ret );

END:
    return ret;        
}

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

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

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

jschultz410
источник
1
Очень хорошо. Я обычно делаю нечто подобное. goto отлично подходит для этого сценария. Единственная разница в том, что я не вижу необходимости в этом последнем "goto END", я просто вставляю успешный возврат в этой точке, а затем неудачный возврат после остальных.
Нил Рой
1
Спасибо @NeilRoy. Причина goto END в том, что мне нравится, что подавляющее большинство моих функций имеют одну точку входа и одну точку выхода. Таким образом, если я хочу добавить некоторую «окончательную» логику к любой функции, я всегда легко могу это сделать, не беспокоясь о том, что где-то скрываются другие скрытые результаты. :)
jschultz410
0

Если вы используете C с Win32, вы можете использовать его структурированную обработку исключений (SEH) для имитации попытки / отлова.

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

onmyway133
источник
-1

Возможно, это не основной язык (к сожалению), но в APL есть операция ⎕EA (расшифровывается как Execute Alternate).

Использование: 'Y' ⎕EA 'X', где X и Y - либо фрагменты кода, представленные в виде строк, либо имен функций.

Если X обнаруживает ошибку, вместо этого будет выполняться Y (обычно обработка ошибок).

mappo
источник
2
Привет, mappo, добро пожаловать в StackOverflow. Хотя это интересно, вопрос был конкретно о том, чтобы сделать это в C. Так что это на самом деле не отвечает на вопрос.
luser droog