O guia de estilo linux fornece razões específicas para o uso de goto
acordo com o seu exemplo:
https://www.kernel.org/doc/Documentation/process/coding-style.rst
A lógica para usar gotos é:
- declarações incondicionais são mais fáceis de entender e seguir
- aninhamento é reduzido
- erros ao não atualizar pontos de saída individuais ao impedir modificações
- salva o trabalho do compilador para otimizar o código redundante;)
Isenção de responsabilidade Não devo compartilhar meu trabalho. Os exemplos aqui são um pouco inventados, então tenha paciência comigo.
Isso é bom para o gerenciamento de memória. Recentemente, trabalhei em códigos que alocavam memória dinamicamente (por exemplo, um char *
retornado por uma função). Uma função que analisa um caminho e verifica se o caminho é válido analisando os tokens do caminho:
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
...
some statements, some involving dynamically allocated memory
...
if ( check_this() ){
free(var1);
free(var2);
...
free(varN);
return 1;
}
...
some more stuff
...
if(something()){
if ( check_that() ){
free(var1);
free(var2);
...
free(varN);
return 1;
} else {
free(var1);
free(var2);
...
free(varN);
return 0;
}
}
token = strtok(NULL,delim);
}
free(var1);
free(var2);
...
free(varN);
return 1;
Agora, para mim, o código a seguir é muito melhor e mais fácil de manter, se você precisar adicionar um varNplus1
:
int retval = 1;
tmp_string = strdup(string);
token = strtok(tmp_string,delim);
while( token != NULL ){
...
some statements, some involving dynamically allocated memory
...
if ( check_this() ){
retval = 1;
goto out_free;
}
...
some more stuff
...
if(something()){
if ( check_that() ){
retval = 1;
goto out_free;
} else {
retval = 0;
goto out_free;
}
}
token = strtok(NULL,delim);
}
out_free:
free(var1);
free(var2);
...
free(varN);
return retval;
Agora, o código tinha todos os tipos de problemas com ele, ou seja, N estava acima de 10, e a função tinha mais de 450 linhas, com 10 níveis de aninhamento em alguns lugares.
Mas eu ofereci ao meu supervisor para refatorá-lo, o que eu fiz e agora são várias funções curtas, e todas elas têm o estilo linux
int function(const char * param)
{
int retval = 1;
char * var1 = fcn_that_returns_dynamically_allocated_string(param);
if( var1 == NULL ){
retval = 0;
goto out;
}
if( isValid(var1) ){
retval = some_function(var1);
goto out_free;
}
if( isGood(var1) ){
retval = 0;
goto out_free;
}
out_free:
free(var1);
out:
return retval;
}
Se considerarmos o equivalente sem goto
s:
int function(const char * param)
{
int retval = 1;
char * var1 = fcn_that_returns_dynamically_allocated_string(param);
if( var1 != NULL ){
if( isValid(var1) ){
retval = some_function(var1);
} else {
if( isGood(var1) ){
retval = 0;
}
}
free(var1);
} else {
retval = 0;
}
return retval;
}
Para mim, no primeiro caso, é óbvio para mim que, se a primeira função retornar NULL
, estaremos fora daqui e voltaremos 0
. No segundo caso, eu tenho que rolar para baixo para ver que o if contém toda a função. Concedido o primeiro indica isso estilisticamente (o nome " out
") e o segundo faz sintaticamente. O primeiro é ainda mais óbvio.
Além disso, eu prefiro ter free()
instruções no final de uma função. Isso ocorre em parte porque, na minha experiência, as free()
declarações no meio de funções cheiram mal e indicam para mim que eu deveria criar uma sub-rotina. Nesse caso, eu criei var1
em minha função e não free()
consegui em uma sub-rotina, mas é por isso que o goto out_free
estilo de sair é tão prático.
Eu acho que os programadores precisam ser educados acreditando que goto
são maus. Então, quando estiverem maduros o suficiente, devem procurar o código fonte do Linux e ler o guia de estilo do Linux.
Devo acrescentar que uso esse estilo de maneira muito consistente, toda função tem um int retval
, um out_free
label e um out label. Devido à consistência estilística, a legibilidade é aprimorada.
Bônus: quebra e continua
Digamos que você tenha um loop while
char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
var1 = functionA(line,count);
var2 = functionB(line,count);
if( functionC(var1, var2){
count++
continue;
}
...
a bunch of statements
...
count++;
free(var1);
free(var2);
}
Há outras coisas erradas nesse código, mas uma coisa é a instrução continue. Eu gostaria de reescrever a coisa toda, mas fui encarregado de modificá-la em um pequeno caminho. Levaria dias para refatorá-lo de uma maneira que me satisfizesse, mas a mudança real foi de cerca de meio dia de trabalho. O problema é que, mesmo se nós ' continue
' ainda precisamos libertar var1
e var2
. Eu tive que adicionar um var3
, e isso me fez querer vomitar ter que espelhar as declarações free ().
Eu era um estagiário relativamente novo na época, mas estava olhando o código-fonte do Linux há algum tempo, então perguntei ao meu supervisor se eu poderia usar uma declaração goto. Ele disse que sim, e eu fiz isso:
char *var1, *var2;
char line[MAX_LINE_LENGTH];
while( sscanf(line,... ){
var1 = functionA(line,count);
var2 = functionB(line,count);
var3 = newFunction(line,count);
if( functionC(var1, var2){
goto next;
}
...
a bunch of statements
...
next:
count++;
free(var1);
free(var2);
}
Eu acho que continua bom, na melhor das hipóteses, mas para mim eles são como um goto com uma etiqueta invisível. O mesmo vale para pausas. Eu ainda preferiria continuar ou interromper, a menos que, como foi o caso aqui, o force a espelhar modificações em vários lugares.
E devo acrescentar que esse uso goto next;
e o next:
rótulo são insatisfatórios para mim. Eles são meramente melhores do que espelhar o free()
's e as count++
declarações.
goto
Quase sempre estão errados, mas é preciso saber quando eles são bons de usar.
Uma coisa que não discuti é o tratamento de erros, coberto por outras respostas.
atuação
Pode-se observar a implementação de strtok () http://opensource.apple.com//source/Libc/Libc-167/string.subproj/strtok.c
#include <stddef.h>
#include <string.h>
char *
strtok(s, delim)
register char *s;
register const char *delim;
{
register char *spanp;
register int c, sc;
char *tok;
static char *last;
if (s == NULL && (s = last) == NULL)
return (NULL);
/*
* Skip (span) leading delimiters (s += strspn(s, delim), sort of).
*/
cont:
c = *s++;
for (spanp = (char *)delim; (sc = *spanp++) != 0;) {
if (c == sc)
goto cont;
}
if (c == 0) { /* no non-delimiter characters */
last = NULL;
return (NULL);
}
tok = s - 1;
/*
* Scan token (scan for delimiters: s += strcspn(s, delim), sort of).
* Note that delim must have one NUL; we stop if we see that, too.
*/
for (;;) {
c = *s++;
spanp = (char *)delim;
do {
if ((sc = *spanp++) == c) {
if (c == 0)
s = NULL;
else
s[-1] = 0;
last = s;
return (tok);
}
} while (sc != 0);
}
/* NOTREACHED */
}
Por favor, corrija-me se estiver errado, mas acredito que o cont:
rótulo e a goto cont;
declaração existem para desempenho (eles certamente não tornam o código mais legível). Eles podem ser substituídos por código legível fazendo
while( isDelim(*s++,delim));
pular delimitadores. Mas, para ser o mais rápido possível e evitar chamadas de função desnecessárias, elas fazem dessa maneira.
Eu li o jornal de Dijkstra e acho bastante esotérico.
google "dijkstra goto declaração considerada prejudicial" porque não tenho reputação suficiente para postar mais de 2 links.
Eu o vi citado como uma razão para não usar o goto e a leitura dele não mudou nada, na medida em que meus usos do goto são considerados.
Adendo :
Eu criei uma regra clara enquanto pensava em tudo isso sobre continuações e interrupções.
- Se em um loop while, você tem uma continuação, o corpo do loop while deve ser uma função e a continuação uma declaração de retorno.
- Se em um loop while, você possui uma instrução break, o próprio loop while deve ser uma função e a interrupção deve se tornar uma declaração de retorno.
- Se você tiver os dois, algo pode estar errado.
Nem sempre é possível devido a problemas de escopo, mas descobri que isso facilita muito o raciocínio sobre meu código. Eu havia notado que sempre que um loop de tempo interrompia ou continuava, isso me dava um mau pressentimento.