Experimente as declarações catch em C


101

Eu estava pensando hoje sobre os blocos try / catch existentes em outras línguas. Pesquisei por um tempo isso, mas sem resultado. Pelo que eu sei, não existe try / catch em C. Porém, existe uma maneira de "simular" eles?
Claro, há assert e outros truques, mas nada como try / catch, que também capturam a exceção levantada. Obrigado


3
Mecanismos de exceção não serão geralmente úteis sem um mecanismo para liberar recursos automaticamente quando a pilha for desfeita. C ++ usa RAII; Java, C #, Python, etc. usam coletores de lixo. (E observe que os coletores de lixo liberam apenas memória. Para liberar automaticamente outros tipos de recursos, eles também adicionam coisas como finalizadores ou gerenciadores de contexto ...)
jamesdlin

@jamesdlin, Por que não conseguimos fazer RAII com C?
Pacerier de

1
@Pacerier RAII requer funções de chamada automaticamente quando os objetos são destruídos (ou seja, destruidores). Como você propõe fazer isso em C?
Jamesdlin

Respostas:


90

C em si não oferece suporte a exceções, mas você pode simulá-las até certo ponto com chamadas setjmpe 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);
}

Este site tem um bom tutorial sobre como simular exceções com setjmpelongjmp


1
solução incrível! esta solução é cruzada? Funcionou para mim no MSVC2012, mas não funcionou no compilador MacOSX Clang.
mannysz

1
me dê uma dica: eu pensei que as cláusulas try catch permitiam que você capturasse exceções (como dividir por zero). Esta função parece permitir apenas que você capture exceções que você mesmo lança. Exceções reais não são lançadas chamando longjmp certo? Se eu usar esse código para fazer algo parecido, try{ x = 7 / 0; } catch(divideByZeroException) {print('divided by zero')}; ele não funcionará certo?
Sam

Devide by zero não é nem mesmo uma exceção em C ++, para tratá-lo você precisa verificar se o divisor não é zero e tratá-lo ou tratar o SIGFPE que é lançado quando você executa uma fórmula devide by zero.
James

25

Use goto em C para situações semelhantes de tratamento de erros.
Esse é o equivalente mais próximo de exceções que você pode obter em C.


3
@JensGustedt É exatamente para isso que goto é usado atualmente com muita frequência e por exemplo onde faz sentido (setjmp / ljmp é a melhor alternativa, mas label + goto é normalmente mais usado).
Tomas Pruzina

1
@AoeAoe, provavelmente gotoé mais usado para tratamento de erros, mas e daí? A questão não é sobre o tratamento de erros como tal, mas explicitamente sobre equivalentes try / catch. gotonão é equivalente a try / catch, pois está restrito à mesma função.
Jens Gustedt

@JensGustedt Eu meio que reagi ao ódio / medo de goto e das pessoas que o usam (meus professores também me contaram histórias assustadoras de uso de goto na universidade). [OT] A única coisa realmente arriscada e 'nebulosa' sobre goto é 'ir para trás', mas eu vi isso no Linux VFS (git blame guy jurou que era crítico-benéfico para o desempenho).
Tomas Pruzina

Veja as fontes systemctl para usos legítimos de gotoum mecanismo try / catch usado em uma fonte moderna, amplamente aceita e revisada por pares. Procure gotoum equivalente de "lançamento" e finishum equivalente de "captura".
Stewart

13

Ok, não resisti em responder a isso. Deixe-me primeiro dizer que não acho uma boa ideia simular isso em C, pois é realmente um conceito estranho para C.

Podemos usar abusar das variáveis de pilha de pré-processamento e locais para dar uso uma versão limitada do C ++ try / lance / catch.

Versão 1 (lances de escopo local)

#include <stdbool.h>

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

A versão 1 é apenas um lançamento local (não pode sair do escopo da função). Ele depende da capacidade do C99 de declarar variáveis ​​no código (deve funcionar no C89 se a tentativa for a primeira coisa na função).

Esta função apenas cria um var local para saber se houve um erro e usa um goto para pular para o bloco catch.

Por exemplo:

#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;
}

Isso funciona para algo como:

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

Versão 2 (salto de escopo)

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

A versão 2 é muito mais complexa, mas basicamente funciona da mesma maneira. Ele usa um salto longo da função atual para o bloco try. O bloco try então usa um if / else para pular o bloco de código para o bloco catch que verifica a variável local para ver se ela deve pegar.

O exemplo expandiu novamente:

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;
}

Este usa um ponteiro global para que longjmp () saiba qual try foi executado pela última vez. Estamos usando abusando da pilha para funções criança também pode ter um bloco try / catch.

Usar este código tem vários aspectos negativos (mas é um exercício mental divertido):

  • Isso não irá liberar memória alocada, pois não há desconstrutores sendo chamados.
  • Você não pode ter mais de 1 tentativa / captura em um escopo (sem aninhamento)
  • Você não pode lançar exceções ou outros dados como em C ++
  • Não é seguro para thread
  • Você está configurando a falha de outros programadores porque eles provavelmente não perceberão o hack e tentarão usá-los como os blocos try / catch do C ++.

boas soluções alternativas.
HaseeB Mir

a versão 1 é uma boa ideia, mas essa variável __HadError precisaria ser redefinida ou ter seu escopo definido. Caso contrário, você não poderá usar mais de um try-catch no mesmo bloco. Talvez use uma função global como bool __ErrorCheck(bool &e){bool _e = e;e=false;return _e;}. Mas a variável local também seria redefinida, então as coisas ficam um pouco fora de controle.
flamewave000

Sim, é limitado a um try-catch na mesma função. Um problema maior do que a variável, entretanto, é o rótulo, pois você não pode ter rótulos duplicados na mesma função.
Paul Hutchinson

10

No C99, você pode usar setjmp/ longjmppara fluxo de controle não local.

Dentro de um único escopo, o padrão de codificação genérico e estruturado para C na presença de múltiplas alocações de recursos e múltiplas saídas usa goto, como neste exemplo . Isso é semelhante a como C ++ implementa chamadas de destruidor de objetos automáticos por baixo do capô e, se você se empenhar nisso, deve permitir um certo grau de limpeza, mesmo em funções complexas.


5

Enquanto algumas das outras respostas cobriram os casos simples usando setjmpe longjmp, em um aplicativo real, há duas preocupações que realmente importam.

  1. Aninhamento de blocos try / catch. Usar uma única variável global para seu jmp_buffará com que isso não funcione.
  2. Threading. Uma única variável global para você jmp_bufcausará todos os tipos de dor nessa situação.

A solução para isso é manter uma pilha de thread local jmp_bufque é atualizada conforme você avança. (Acho que é isso que lua usa internamente).

Então, em vez disso (da resposta incrível de 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);
}

Você usaria algo como:

#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);
}

Novamente, uma versão mais realista disso incluiria alguma maneira de armazenar informações de erro no exception_state, melhor tratamento de MAX_EXCEPTION_DEPTH(talvez usando realloc para aumentar o buffer ou algo parecido).

AVISO LEGAL: O código acima foi escrito sem qualquer teste. É puramente para que você tenha uma ideia de como estruturar as coisas. Diferentes sistemas e diferentes compiladores precisarão implementar o armazenamento local do thread de maneira diferente. O código provavelmente contém erros de compilação e erros de lógica - então, enquanto você estiver livre para usá-lo como quiser, TESTE-o antes de usá-lo;)


4

Uma rápida pesquisa no google produz soluções kludgey como esta, que usam setjmp / longjmp como outros mencionaram. Nada tão simples e elegante quanto o try / catch de C ++ / Java. Sou bastante parcial com a exceção de Ada lidando comigo mesma.

Verifique tudo com as declarações if :)


4

Isso pode ser feito com o setjmp/longjmpC. P99 tem um conjunto de ferramentas bastante confortável para isso que também é consistente com o novo modelo de rosca do C11.


2

Esta é outra maneira de lidar com erros em C, que tem mais desempenho do que usar setjmp / longjmp. Infelizmente, não funcionará com o MSVC, mas se usar apenas GCC / Clang for uma opção, você deve considerá-la. Especificamente, ele usa a extensão "rótulo como valor", que permite que você pegue o endereço de um rótulo, armazene-o em um valor e salte para ele incondicionalmente. Vou apresentá-lo usando um exemplo:

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;
}

Se desejar, você pode refatorar o código comum em define, implementando efetivamente seu próprio sistema de tratamento de erros.

/* 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); }

Então o exemplo se torna

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

Aviso: o seguinte não é muito bom, mas faz o trabalho.

#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)

Uso:

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

Resultado:

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 

Lembre-se de que isso está usando funções aninhadas e __COUNTER__. Você estará seguro se estiver usando o gcc.


1

Redis usa goto para simular try / catch, IMHO é muito limpo e elegante:

/* 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;
}

O código está quebrado. errnosó deve ser usado logo após a falha na chamada do sistema e não três chamadas depois.
ceving

Este código duplica a lógica de tratamento de erros em vários lugares e pode fazer coisas incorretas, como chamar fclose (fp) várias vezes. Seria muito melhor usar vários rótulos e codificar o que ainda precisa ser recuperado usando esses rótulos (em vez de apenas um para todos os erros) e, em seguida, pular para o local correto de tratamento de erros, dependendo de onde o erro ocorre no código.
jschultz410

1

Em C, você pode "simular" exceções junto com a "recuperação de objeto" automática por meio do uso manual de if + goto para tratamento explícito de erros.

Costumo escrever código C como o seguinte (resumido para destacar o tratamento de erros):

#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;        
}

Isso é ANSI C completamente padrão, separa o tratamento de erros do seu código principal, permite o desenrolamento (manual) da pilha de objetos inicializados de maneira muito semelhante à do C ++ e é completamente óbvio o que está acontecendo aqui. Como você está testando explicitamente a falha em cada ponto, é mais fácil inserir logs específicos ou tratamento de erros em cada lugar em que um erro pode ocorrer.

Se você não se importa com um pouco de magia de macro, pode tornar isso mais conciso enquanto faz outras coisas, como registrar erros com rastreamentos de pilha. Por exemplo:

#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;        
}

Claro, isso não é tão elegante quanto exceções C ++ + destruidores. Por exemplo, aninhar várias pilhas de tratamento de erros em uma função dessa maneira não é muito limpo. Em vez disso, você provavelmente gostaria de dividi-las em subfunções independentes que lidam de forma semelhante com erros, inicializar + finalizar explicitamente assim.

Isso também funciona apenas dentro de uma única função e não continuará pulando na pilha a menos que chamadores de nível superior implementem uma lógica de tratamento de erros explícita semelhante, enquanto uma exceção C ++ continuará pulando na pilha até encontrar um manipulador apropriado. Nem permite que você lance um tipo arbitrário, mas apenas um código de erro.

A codificação sistemática dessa forma (ou seja, com uma única entrada e um único ponto de saída) também torna muito fácil inserir a lógica pré e pós ("finalmente") que será executada independentemente do que aconteça. Você acabou de colocar sua lógica "finalmente" após o rótulo END.


1
Muito agradável. Tenho tendência a fazer algo semelhante. goto é ótimo para este cenário. A única diferença é que não vejo a necessidade desse último "goto END", apenas insiro um retorno de sucesso naquele ponto, um retorno de falha após o resto.
Neil Roy

1
Obrigado @NeilRoy. O motivo do goto END é que gosto que a grande maioria das minhas funções tenham um único ponto de entrada e um único ponto de saída. Dessa forma, se eu quiser adicionar alguma lógica "finalmente" a qualquer função, sempre posso facilmente, sem precisar me preocupar, se há outros retornos ocultos espreitando em algum lugar. :)
jschultz410

0

Se estiver usando C com Win32, você pode aproveitar seu Structured Exception Handling (SEH) para simular try / catch.

Se você estiver usando C em plataformas que não suportam setjmp()e longjmp(), dê uma olhada neste Tratamento de exceções da biblioteca pjsip, ele fornece sua própria implementação


-1

Talvez não seja um idioma principal (infelizmente), mas em APL, existe a operação ⎕EA (que significa Execute Alternate).

Uso: 'Y' ⎕EA 'X' onde X e Y são trechos de código fornecidos como strings ou nomes de função.

Se X der certo, Y (geralmente tratamento de erros) será executado.


2
Olá, mappo, bem-vindo ao StackOverflow. Embora interessante, a questão era especificamente sobre fazer isso em C. Portanto, isso realmente não responde à pergunta.
luser droog
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.