Paulo Henrique Rodrigues Pinheiro

Um blog sobre programação para programadores!


Linguagem C: goto é para inocentes... O lance é setjmp!

Usando goto e setjmp para manter o código legível.

xkcd goto

For a number of years I have been familiar with the observation that the quality of programmers is a decreasing function of the density of go to statements in the programs they produce. More recently I discovered why the use of the go to statement has such disastrous effects, and I became convinced that the go to statement should be abolished from all "higher level" programming languages. - Edsger W. Dijkstra

É uma discussão acalorada. Mas eu afirmo que é preciso, eventualmente, usar um goto em C. E com o avanço da programação funcional, tem-se percebido como o Dijkstra estava certo. Mas estamos programando em C, e não em uma linguagem de "alto nível".

Liberação de memória, retorno de funções, saída clara de loops aninhados... Em alguns casos pode ficar menos poluído usar um goto.

Mas o goto tem um primo rico. É o setjmp. O código em C costuma ficar bem complexo quando as coisas vão ficando sérias. Em alguns momentos fica bem difícil escrever programas robustos.

Por exemplo, imagine que uma função retorne um tipo inteiro, mas deve poder retornar um código de erro também... Pode-se usar uma variável global, como nesse exemplo:


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

int error;

int my_f(int a, int b) {
    if(b==0) {
        error = 1;
        return error;
    }

    error = 0;
    return a/b;
}

int main(void) {
    int result;

    result = my_f(1, 0);
    if(1==error) {
        puts("Error in my_f().");
        return EXIT_FAILURE;
    }

    printf("Result: %d\n", result);

    return EXIT_SUCCESS;
}

É muito fácil esquecer de alguma coisa e demorar pra perceber.

Ou então passa-se um ponteiro para um endereço em que será informado o status de retorno da função:


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

int my_f(int a, int b, int *response) {
    if(b==0) {
        return (*response = -1);
    }

    *response = 0;

    return a/b;
}

int main(void) {
    int result, response;

    result = my_f(1, 0, &response);
    if(-1==response) {
        puts("Error in my_f().");
        return EXIT_FAILURE;
    }

    printf("Result: %d\n", result);

    return EXIT_SUCCESS;
}

Eu não gosto disso, pois polui as chamadas a função.

Por fim, pode-se usar uma estrutura como retorno, contendo o valor e um código de erro:


typedef struct {
    int result;
    int response;
} f_return;

Essa estrutura então pode ser passada como referência:


void my_f(int a, int b, f_return *response) {
    if(b==0) {
        response->response = -1;
    }
    else {
        response->response = 0;
        response->result = a/b;
    }
}

Eu sou mais simpático a essa solução, especialmente depois de começar a estudar Rust, que tem esse padrão para tratamento de erros.

Em C ainda, temos outras possibilidades, como ter essa estrutura como uma variável global, ou ainda alocada dinamicamente na função chamada, tendo seu ponteiro retornado, o que exige que não esqueçamos de liberar essa memória alocada.

Mas há uma prática entre programadores C, em usar instruções de salto longo, para lidar com essa situação.

Voltemos ao nosso exemplo, em que corremos o risco de executar uma divisão por zero. Devemos verificar e evitar essa tragédia. Mas como relatá-la?

Podemos emular uma exceção. Um try / catch se preferir.

Por exemplo, ao invés de retornar um código de erro ou encerrar com um abort, pode-se, do ponto de vista do "usuário" de sua biblioteca, retornar com um fatality :) pra cima dele:


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

jmp_buf jmp_buffer;

enum { OK, ERR_DIV_BY_ZERO };

int my_f(int a, int b) {
    if(b==0) {
        longjmp(jmp_buffer, ERR_DIV_BY_ZERO);
    }

    return a/b;
}

int main(void) {
    int result, error;

    error = setjmp(jmp_buffer);

    if(OK==error) { /* try */
        result = my_f(1, 0);
    }
    else { /* catch */
        puts("Error in my_f().");
        return EXIT_FAILURE;
    }

    printf("Result: %d\n", result);

    return EXIT_SUCCESS;
}

Executando esse código (salvei esse programa como test_jmp.c) temos:


$ gcc -Wall -ansi -pedantic test_jmp.c -o test_jmp
$ ./test_jmp
Error in my_f().
$

E alterando result = my_f(1, 0); para result = my_f(10, 2);:


$ gcc -Wall -ansi -pedantic test_jmp.c -o test_jmp
$ ./test_jmp
Result: 5
$

É um código estranho, quando nos deparamos com ele pela primeira vez, mas vamos debulhar esse trem!

Começando do main, temos a linha error = setjmp(jmp_buffer); que nos indica o exato local de retorno, caso algo de errado. Quando o programa é executado, nesse ponto ela retornará 0 (zero), pois fomos nós quem a chamamos. Nosso OK tem esse valor, pois é a primeira entrada do enum.

Como 0 é OK, e error tem esse valor, a função my_f será chamada.

Em nossa função, caso esteja tudo certo, ela retorna normalmente e o programa executa conforme o fluxo normal.

Caso o argumento b seja 0, não podemos realizar a divisão e chamamos longjmp, indicando para qual ponto do programa ele deve retornar (primeiro parâmetro), e um código de retorno (segundo parâmetro).

Como no início salvamos o stack com a primeira chamada a setjmp, nossa função "sabe" aonde retornar: para a linha error = setjmp(jmp_buffer);. Novamente, o if é executado, só que desta vez o valor de error é ERR_DIV_BY_ZERO, o que faz com que a mensagem de erro seja mostrada, e o programa termine com status de erro (return EXIT_FAILURE;).

É estranho, mas se seguir esse roteiro descrito acima, não haverá mais estranhamento se deparar-se com esse tipo de construção.

Há quem utilize macros para isso, para tornar o código mais legível. Eu não simpatizo com essa abordagem, até porque, se alguém prefere esse tipo de sintaxe, pode passar a programar em C++ :P.