Спробуйте увійти в C


101

Я сьогодні думав про блоки "пробувати / ловити", що існують іншими мовами. На деякий час це гуглило, але безрезультатно. З того, що я знаю, не існує такого поняття, як спробувати / зловити в C. Однак, чи є спосіб їх "імітувати"?
Звичайно, є твердження та інші хитрощі, але нічого подібного до спроби / лову, що також сприймає підвищене виключення. Дякую


3
Механізми, що нагадують винятки, не будуть загалом корисними без механізму автоматичного звільнення ресурсів, коли стек розкручений. C ++ використовує RAII; Java, C #, Python тощо використовують сміттєзбірники. (І зауважте, що сміттєзбірники звільняють лише пам'ять. Щоб автоматично звільнити інші типи ресурсів, вони також додають такі речі, як фіналізатори чи контекстні менеджери ...)
jamesdlin

@jamesdlin, чому ми не могли зробити RAII з C?
Pacerier

1
@Pacerier RAII вимагає автоматичного виклику функцій при знищенні об'єктів (тобто деструкторів). Як ви пропонуєте зробити це в C?
jamesdlin

Відповіді:


90

Сам 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

1
підкажіть мене: я думав, що спробувальні пропозиції лову дозволяють ловити винятки (наприклад, ділення на нуль). Ця функція, здається, дозволяє лише ловити винятки, які ви кидаєте на себе. Реальні винятки не кидаються, зателефонувавши 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не є еквівалентом для спроб / лову, оскільки він обмежений однією і тією ж функцією.
Йенс Гуведт

@JensGustedt Я якось відреагував на ненависть / страх перед гото та людьми, які його використовують (мої викладачі розповідали мені страшні історії використання гото в університеті теж). [OT] Єдине, що є дійсно, дуже ризикованим і "похмурим" питанням про Goto, це "повернутися назад", але я бачив, що в Linux VFS (хлопець, що звинувачує, поклявся, що це критично-вигідне виконання).
Томаш Прузіна

Перегляньте джерела systemctl щодо законних цілей використання gotoяк механізму спроб / лову, який використовується в сучасному, широко сприйнятому, рецензованому джерелі. Шукайте gotoеквівалент "кидка" та еквівалент finish"лову".
Стюарт

13

Гаразд, я не втримався відповісти на це. Дозвольте спершу сказати, що я не думаю, що це сильна ідея моделювати це в C, оскільки це дійсно є іноземною концепцією для C.

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

Версія 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, якщо спроба є першим ділом у функції).

Ця функція просто створює локальний var, щоб він знав, чи сталася помилка, і використовує goto, щоб перейти до блоку вилову.

Наприклад:

#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 набагато складніша, але в основному працює так само. Він використовує довгий стрибок з поточної функції до блоку спробу. Блок спробу потім використовує 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 ++
  • Не безпечно для ниток
  • Ви налаштовуєте інших програмістів на збій, оскільки вони, ймовірно, не помітять злому, і спробують використовувати їх, як C ++ блоки "try / catch".

приємні альтернативні рішення.
HaseeB Mir

версія 1 - хороша ідея, але цю __HadError змінну потрібно буде скинути чи визначити. Інакше ви не зможете використовувати більше одного пробного лову в одному блоці. Можливо, використовувати глобальну функцію на кшталт bool __ErrorCheck(bool &e){bool _e = e;e=false;return _e;}. Але локальна змінна також буде переосмислена, тому справи трохи виходять з-під руки.
flamewave000

Так, він обмежений одним пробним уловом тієї ж функції. Більша проблема, ніж змінна, проте є міткою, оскільки ви не можете мати дублікатів міток в одній функції.
Пол Хатчінсон

10

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

У межах однієї області загальної, структурованої схеми кодування для C за наявності декількох розподілів ресурсів та використання декількох виходів goto, як у цьому прикладі . Це схоже на те, як C ++ реалізує виклики деструкторів автоматичних об'єктів під кришкою, і якщо ви старанно дотримуєтесь цього, це повинно забезпечувати певну чистоту навіть у складних функціях.


5

Хоча деякі інші відповіді висвітлювали прості випадки використання, setjmpа longjmpв реальній заявці є дві проблеми, які насправді мають значення.

  1. Введення блоків спробу / лову. Використання єдиної глобальної змінної для вашогоjmp_buf змусить їх не працювати.
  2. Нитки. Єдина глобальна змінна для вас jmp_bufзаподіює всілякі болі в цій ситуації.

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

Тож замість цього (з дивовижної відповіді ДжаредПара)

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, як згадували інші. Нічого так просто і елегантно, як C ++ / Java's try / catch. Я скоріше частковий до винятку Ада.

Перевірте все, якщо заяви :)


4

Це можна зробити setjmp/longjmpв C. 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;
}

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, ви будете в безпеці.


1

Redis використовує goto для імітації спробу / лову, 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повинні використовуватися лише після невдалого системного виклику, а не через три дзвінки пізніше.
ceving

Цей код дублює логіку обробки помилок у кількох місцях і може робити неправильні речі, такі як виклик fclose (fp) кілька разів. Було б набагато краще використовувати кілька міток і кодувати те, що ще потрібно відшкодувати за допомогою цих міток (а не лише одну для всіх помилок), а потім перейти на правильне місце обробки помилок залежно від того, де в коді виникає помилка.
jschultz410

1

У C ви можете "імітувати" винятки разом з автоматичною "рекультивацією об'єктів" шляхом ручного використання if + goto для явної обробки помилок.

Я часто пишу код С на зразок наступного (зводиться до виділення помилок):

#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 ++ просто продовжує стрибати стек, поки не знайде відповідний обробник. Він також не дозволяє викидати довільний тип, а натомість лише код помилки.

Систематичне кодування таким чином (тобто, з єдиним входом і єдиною точкою виходу) також дозволяє дуже легко вставити перед і після ("нарешті") логіку, яка буде виконуватися незалежно від того. Ви просто ставите свою "остаточну" логіку після мітки END.


1
Дуже хороша. Я схильний робити щось подібне. goto чудово підходить для цього сценарію. Єдина відмінність полягає в тому, що я не бачу потреби в тому останньому "goto END", я просто вставляю успіх у цьому пункті, провал повернення після решти.
Ніл Рой

1
Дякую @NeilRoy Причина goto END полягає в тому, що мені подобається, що переважна більшість моїх функцій мають єдину точку входу та одну точку виходу. Таким чином, якщо я хочу додати якусь "остаточну" логіку до будь-якої функції, яку я завжди легко можу, не турбуючись, десь ховається десь інша прихована віддача. :)
jschultz410

0

Якщо ви використовуєте C з Win32, ви можете використовувати його обробку структурованих винятків (SEH) для імітації спроб / лову.

Якщо ви використовуєте C в платформах , які не підтримують setjmp()і longjmp(), подивіться на це Exception Handling бібліотеки pjsip, він надає свою власну реалізацію


-1

Можливо, це не основна мова (на жаль), але в APL є операція ⎕EA (стенд для Execute Alternate).

Використання: 'Y' ⎕EA 'X', де X і Y або фрагменти коду, подані у вигляді рядків або імен функцій.

Якщо X стикається з помилкою, замість цього буде виконуватися Y (як правило, помилка).


2
Привіт mappo, ласкаво просимо до StackOverflow. Хоча цікаво, питання стосувалось саме цього в C. Тому це насправді не відповідає на питання.
luser droog
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.