Pruebe las declaraciones de captura en C

101

Hoy estaba pensando en los bloques try / catch existentes en otros idiomas. Busqué en Google por un tiempo, pero sin resultado. Por lo que sé, no existe el try / catch en C. Sin embargo, ¿hay alguna manera de "simularlos"?
Claro, hay aserción y otros trucos, pero nada como try / catch, que también capturan la excepción generada. Gracias

Andrés
fuente
3
Los mecanismos de tipo excepción no serán generalmente útiles sin un mecanismo para liberar recursos automáticamente cuando se desenrolla la pila. C ++ usa RAII; Java, C #, Python, etc. usan recolectores de basura. (Y tenga en cuenta que los recolectores de basura solo liberan memoria. Para liberar automáticamente otros tipos de recursos, también agregan cosas como finalizadores o administradores de contexto ...)
jamesdlin
@jamesdlin, ¿Por qué no pudimos hacer RAII con C?
Pacerier
1
@Pacerier RAII requiere llamar a funciones automáticamente cuando se destruyen objetos (es decir, destructores). ¿Cómo propones hacer eso en C?
jamesdlin

Respuestas:

90

C en sí no admite excepciones, pero puede simularlas hasta cierto punto con setjmpy longjmpllamadas.

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 sitio web tiene un buen tutorial sobre cómo simular excepciones con setjmpylongjmp

JaredPar
fuente
1
increíble solución! ¿Es esta solución cruzada? Me funcionó en MSVC2012 pero no en el compilador MacOSX Clang.
mannysz
1
dame una pista: pensé que las cláusulas try catch te permitían detectar excepciones (como dividir por cero). Esta función parece solo permitirle detectar excepciones que usted mismo lanza. Las excepciones reales no se lanzan llamando a longjmp, ¿verdad? Si utilizo este código para hacer algo así try{ x = 7 / 0; } catch(divideByZeroException) {print('divided by zero')}; , ¿no funcionará bien?
Sam
Devide by zero ni siquiera es una excepción en C ++, para manejarlo, debe verificar que el divisor no sea cero y manejarlo o manejar el SIGFPE que se lanza cuando ejecuta una fórmula devide by zero.
James
25

Utiliza goto en C para situaciones similares de manejo de errores.
Ese es el equivalente más cercano de excepciones que puede obtener en C.

Alok Save
fuente
3
@JensGustedt Esto es exactamente para lo que se usa actualmente goto con mucha frecuencia y un ejemplo donde tiene sentido (setjmp / ljmp es una mejor alternativa, pero label + goto generalmente se usa más).
Tomas Pruzina
1
@AoeAoe, probablemente gotose usa más para el manejo de errores, pero ¿y qué? La pregunta no es sobre el manejo de errores como tal, sino explícitamente sobre los equivalentes try / catch. gotono es equivalente a fro try / catch ya que está restringido a la misma función.
Jens Gustedt
@JensGustedt Reaccioné un poco hacia el odio / miedo al goto y las personas que lo usan (mis profesores también me contaron historias de miedo sobre el uso del goto en la universidad). [OT] Lo único que es realmente, realmente arriesgado y "turbio" acerca de goto es "ir al revés", pero lo he visto en Linux VFS (el tipo de culpa de git juró que el rendimiento era crítico y beneficioso).
Tomas Pruzina
Consulte las fuentes systemctl para conocer los usos legítimos de gotocomo un mecanismo de prueba / captura utilizado en una fuente moderna, ampliamente aceptada y revisada por pares. Busque gotoun equivalente de "lanzamiento" y finishun equivalente de "captura".
Stewart
13

Ok, no pude resistirme a responder a esto. Permítanme decir primero que no creo que sea una buena idea simular esto en C, ya que realmente es un concepto ajeno a C.

Podemos utilizar abusar de las variables de pila del preprocesador y locales para dar el uso de una versión limitada de C ++ try / tiro / catch.

Versión 1 (lanzamientos de alcance local)

#include <stdbool.h>

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

La versión 1 es solo un lanzamiento local (no puede salir del alcance de la función). Se basa en la capacidad de C99 para declarar variables en el código (debería funcionar en C89 si el intento es lo primero en la función).

Esta función solo crea una var local para que sepa si hubo un error y usa un goto para saltar al bloque catch.

Por ejemplo:

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

Esto resulta en 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;
}

Versión 2 (salto de alcance)

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

La versión 2 es mucho más compleja pero básicamente funciona de la misma manera. Utiliza un salto largo de la función actual al bloque try. El bloque try luego usa un if / else para saltar el bloque de código al bloque catch que verifica la variable local para ver si debería atrapar.

El ejemplo se expandió nuevamente:

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

Esto usa un puntero global para que longjmp () sepa qué intento se ejecutó por última vez. Estamos usando abusar de la pila para que las funciones secundarias también puedan tener un bloque try / catch.

El uso de este código tiene varias desventajas (pero es un ejercicio mental divertido):

  • No liberará la memoria asignada ya que no se están llamando deconstructores.
  • No puede tener más de 1 intento / captura en un alcance (sin anidamiento)
  • En realidad, no puede lanzar excepciones u otros datos como en C ++
  • No es seguro para subprocesos en absoluto
  • Está preparando a otros programadores para que fallen porque probablemente no notarán el hack y tratarán de usarlos como bloques try / catch de C ++.
Paul Hutchinson
fuente
buenas soluciones alternativas.
HaseeB Mir
la versión 1 es una buena idea, pero esa variable __HadError debería restablecerse o ajustarse. De lo contrario, no podrá utilizar más de un try-catch en el mismo bloque. Quizás use una función global como bool __ErrorCheck(bool &e){bool _e = e;e=false;return _e;}. Pero la variable local también se redefiniría, por lo que las cosas se saldrían un poco de control.
flamewave000
Sí, está limitado a un try-catch en la misma función. Sin embargo, un problema mayor que la variable es la etiqueta, ya que no puede tener etiquetas duplicadas en la misma función.
Paul Hutchinson
10

En C99, puede usar setjmp/longjmp para un flujo de control no local.

Dentro de un solo alcance, el patrón de codificación estructurado genérico para C en presencia de múltiples asignaciones de recursos y múltiples usos de salidas goto, como en este ejemplo . Esto es similar a cómo C ++ implementa llamadas de destructor de objetos automáticos bajo el capó, y si se apega a esto diligentemente, debería permitirle un cierto grado de limpieza incluso en funciones complejas.

Kerrek SB
fuente
5

Si bien algunas de las otras respuestas han cubierto los casos simples usando setjmpy longjmp, en una aplicación real hay dos preocupaciones que realmente importan.

  1. Anidamiento de bloques try / catch. Usando una sola variable global para sujmp_buf hará que estos no funcionen.
  2. Enhebrado. Una sola variable global para usted jmp_bufcausará todo tipo de dolor en esta situación.

La solución a estos es mantener una pila local de subprocesos de jmp_buf que se actualiza a medida que avanza. (Creo que esto es lo que lua usa internamente).

Entonces, en lugar de esto (de la increíble respuesta 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);
}

Usarías 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);
}

Una vez más, una versión más realista de esto incluiría alguna forma de almacenar información de error en el exception_statemejor manejo deMAX_EXCEPTION_DEPTH (tal vez usando realloc para hacer crecer el búfer, o algo así).

DESCARGO DE RESPONSABILIDAD: El código anterior se escribió sin ningún tipo de prueba. Es simplemente para que tengas una idea de cómo estructurar las cosas. Diferentes sistemas y diferentes compiladores necesitarán implementar el almacenamiento local de subprocesos de manera diferente. El código probablemente contiene errores de compilación y errores lógicos, por lo que, si bien puede usarlo como desee, PRUEBE antes de usarlo;)

Michael Anderson
fuente
4

Una búsqueda rápida en Google produce soluciones tontas como esta que usan setjmp / longjmp como otros han mencionado. Nada tan sencillo y elegante como el try / catch de C ++ / Java. Soy bastante parcial a la excepción de Ada manejándome.

Verifique todo con declaraciones if :)

James Adam
fuente
4

Esto se puede hacer con setjmp/longjmpC. P99 tiene un conjunto de herramientas bastante cómodo para esto que también es consistente con el nuevo modelo de rosca de C11.

Jens Gustedt
fuente
2

Esta es otra forma de manejar errores en C que es más eficiente que usar setjmp / longjmp. Desafortunadamente, no funcionará con MSVC, pero si usar solo GCC / Clang es una opción, entonces puede considerarlo. Específicamente, utiliza la extensión "etiqueta como valor", que le permite tomar la dirección de una etiqueta, almacenarla en un valor y saltar a ella incondicionalmente. Lo presentaré usando un ejemplo:

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

Si lo desea, puede refactorizar el código común en define, implementando efectivamente su propio sistema de manejo de errores.

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

Entonces el ejemplo se convierte en

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
fuente
2

Advertencia: lo siguiente no es muy agradable, pero funciona.

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

Salida:

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 

Tenga en cuenta que esto está usando funciones anidadas y __COUNTER__. Estará en el lado seguro si está usando gcc.

Naheel
fuente
1

Redis usa goto para simular try / catch, en mi humilde opinión es muy limpio y 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;
}
Forrest Ye
fuente
El código está roto. errnosolo debe usarse justo después de la llamada al sistema fallida y no tres llamadas después.
hasta el
Este código duplica la lógica de manejo de errores en varios lugares y puede hacer cosas incorrectas como llamar a fclose (fp) varias veces. Sería mucho mejor usar varias etiquetas y codificar lo que aún necesita ser reclamado usando esas etiquetas (en lugar de solo una para todos los errores) y luego saltar al lugar correcto de manejo de errores dependiendo de en qué parte del código ocurra el error.
jschultz410
1

En C, puede "simular" excepciones junto con la "recuperación de objetos" automática mediante el uso manual de if + goto para el manejo explícito de errores.

A menudo escribo código C como el siguiente (resumido para resaltar el manejo de errores):

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

Este es ANSI C completamente estándar, separa el manejo de errores de su código de línea principal, permite el desenrollado (manual) de la pila de objetos inicializados al igual que lo hace C ++, y es completamente obvio lo que está sucediendo aquí. Debido a que está probando explícitamente la falla en cada punto, hace que sea más fácil insertar registros específicos o manejo de errores en cada lugar donde puede ocurrir un error.

Si no le importa un poco de magia macro, puede hacerlo más conciso mientras hace otras cosas, como registrar errores con seguimientos de pila. Por ejemplo:

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

Por supuesto, esto no es tan elegante como las excepciones + destructores de C ++. Por ejemplo, anidar múltiples pilas de manejo de errores dentro de una función de esta manera no es muy limpio. En su lugar, probablemente desee dividirlos en subfunciones autónomas que manejen los errores de manera similar, inicializar + finalizar explícitamente de esta manera.

Esto también solo funciona dentro de una sola función y no seguirá saltando en la pila a menos que los llamadores de nivel superior implementen una lógica de manejo de errores explícita similar, mientras que una excepción de C ++ seguirá saltando en la pila hasta que encuentre un controlador apropiado. Tampoco le permite lanzar un tipo arbitrario, sino solo un código de error.

La codificación sistemática de esta manera (es decir, con una sola entrada y un solo punto de salida) también hace que sea muy fácil insertar lógica pre y post ("finalmente") que se ejecutará sin importar qué. Simplemente coloque su lógica "finalmente" después de la etiqueta FIN.

jschultz410
fuente
1
Muy agradable. Tiendo a hacer algo similar. goto es genial para este escenario. La única diferencia es que no veo la necesidad de ese último "goto END", solo inserto un retorno exitoso en ese punto, un retorno fallido después del resto.
Neil Roy
1
Gracias @NeilRoy El motivo del goto END es que me gusta que la gran mayoría de mis funciones tengan un único punto de entrada y un único punto de salida. De esa manera, si quiero agregar algo de lógica "finalmente" a cualquier función, siempre puedo hacerlo fácilmente sin tener que preocuparme de que haya otros retornos ocultos al acecho en alguna parte. :)
jschultz410
0

Si está utilizando C con Win32, puede aprovechar su Manejo de excepciones estructurado (SEH) para simular try / catch.

Si está utilizando C en plataformas que no son compatibles setjmp()y longjmp(), eche un vistazo a este Manejo de excepciones de la biblioteca pjsip, proporciona su propia implementación

onmyway133
fuente
-1

Quizás no sea un idioma importante (desafortunadamente), pero en APL, existe la operación ⎕EA (significa Execute Alternate).

Uso: 'Y' ⎕EA 'X' donde X e Y son fragmentos de código suministrados como cadenas o nombres de funciones.

Si X se encuentra con un error, se ejecutará Y (generalmente manejo de errores) en su lugar.

mappo
fuente
2
Hola mappo, bienvenido a StackOverflow. Si bien es interesante, la pregunta se refería específicamente a hacer esto en C. Entonces, esto realmente no responde a la pregunta.
luser droog