Esta questão é na verdade o resultado de uma discussão interessante em programming.reddit.com há algum tempo. Basicamente, ele se resume ao seguinte código:
int foo(int bar)
{
int return_value = 0;
if (!do_something( bar )) {
goto error_1;
}
if (!init_stuff( bar )) {
goto error_2;
}
if (!prepare_stuff( bar )) {
goto error_3;
}
return_value = do_the_thing( bar );
error_3:
cleanup_3();
error_2:
cleanup_2();
error_1:
cleanup_1();
return return_value;
}
O uso de goto
here parece ser o melhor caminho a seguir, resultando no código mais limpo e eficiente de todas as possibilidades, ou pelo menos é o que me parece. Citando Steve McConnell em Code Complete :
O goto é útil em uma rotina que aloca recursos, executa operações nesses recursos e, em seguida, desaloca os recursos. Com um goto, você pode limpar em uma seção do código. O goto reduz a probabilidade de você se esquecer de desalocar os recursos em cada lugar em que detectar um erro.
Outro suporte para essa abordagem vem do livro Linux Device Drivers , nesta seção .
O que você acha? Este caso é um uso válido para goto
em C? Você prefere outros métodos, que produzem código mais complicado e / ou menos eficiente, mas evita goto
?
fonte
on error goto error
sistema de tratamento de erros :)Respostas:
FWIF, acho que a linguagem de tratamento de erros que você deu no exemplo da pergunta é mais legível e fácil de entender do que qualquer uma das alternativas fornecidas nas respostas até agora. Embora
goto
seja uma má ideia em geral, pode ser útil para o tratamento de erros quando feito de maneira simples e uniforme. Nessa situação, embora seja umgoto
, está sendo usado de forma bem definida e mais ou menos estruturada.fonte
Como regra geral, evitar goto é uma boa ideia, mas os abusos que prevaleciam quando Dijkstra escreveu 'GOTO Considered Harmful' nem passam pela cabeça da maioria das pessoas como uma opção atualmente.
O que você delineou é uma solução generalizável para o problema de tratamento de erros - está bom para mim, desde que seja usada com cuidado.
Seu exemplo particular pode ser simplificado da seguinte forma (etapa 1):
Continuando o processo:
Isso é, acredito, equivalente ao código original. Isso parece particularmente limpo, pois o próprio código original era muito limpo e bem organizado. Freqüentemente, os fragmentos de código não são tão organizados assim (embora eu aceite o argumento de que deveriam ser); por exemplo, freqüentemente há mais estado para passar para as rotinas de inicialização (configuração) do que mostrado e, portanto, mais estado para passar para as rotinas de limpeza também.
fonte
Estou surpreso que ninguém tenha sugerido essa alternativa, então, embora a questão já esteja por aí, vou acrescentá-la: uma boa maneira de resolver esse problema é usar variáveis para rastrear o estado atual. Essa é uma técnica que pode ser usada, quer seja usada ou não
goto
para chegar ao código de limpeza. Como qualquer técnica de codificação, ela tem prós e contras e não é adequada para todas as situações, mas se você estiver escolhendo um estilo, vale a pena considerar - especialmente se você quiser evitargoto
sem terminar comif
s profundamente aninhados .A ideia básica é que, para cada ação de limpeza que precise ser realizada, existe uma variável de cujo valor podemos dizer se a limpeza precisa ser executada ou não.
Vou mostrar a
goto
versão primeiro, porque está mais próxima do código da pergunta original.Uma vantagem disso sobre algumas das outras técnicas é que, se a ordem das funções de inicialização for alterada, a limpeza correta ainda acontecerá - por exemplo, usando o
switch
método descrito em outra resposta, se a ordem de inicialização mudar, então oswitch
deve ser editado com muito cuidado para evitar tentar limpar algo que não foi inicializado em primeiro lugar.Agora, alguns podem argumentar que esse método adiciona muitas variáveis extras - e de fato, neste caso isso é verdade - mas na prática, muitas vezes uma variável existente já rastreia, ou pode ser feita para rastrear, o estado necessário. Por exemplo, se
prepare_stuff()
for realmente uma chamada paramalloc()
, ou paraopen()
, então a variável que contém o ponteiro retornado ou o descritor de arquivo pode ser usada - por exemplo:Agora, se rastrearmos adicionalmente o status de erro com uma variável, podemos evitar
goto
totalmente, e ainda limpar corretamente, sem ter um recuo que fica cada vez mais profundo quanto mais inicialização precisamos:Mais uma vez, existem potenciais críticas a isto:
if (oksofar)
verificações com falha para um único salto para o código de limpeza (o GCC certamente o faz) - e em qualquer caso, o caso de erro geralmente é menos crítico para o desempenho.Isso não é adicionar mais uma variável? Nesse caso, sim, mas muitas vezes a
return_value
variável pode ser usada para desempenhar o papel queoksofar
está desempenhando aqui. Se você estruturar suas funções para retornar erros de maneira consistente, poderá até evitar o segundoif
em cada caso:Uma das vantagens de codificar assim é que a consistência significa que qualquer lugar onde o programador original se esqueceu de verificar o valor de retorno se destaca como um polegar dolorido, tornando muito mais fácil encontrar (aquela classe de) bugs.
Portanto - este é (ainda) mais um estilo que pode ser usado para resolver este problema. Usado corretamente, ele permite um código muito limpo e consistente - e, como qualquer técnica, nas mãos erradas pode acabar produzindo um código prolixo e confuso :-)
fonte
O problema com a
goto
palavra-chave é geralmente mal compreendido. Não é totalmente mau. Você só precisa estar ciente dos caminhos de controle extras que você cria a cada goto. Torna-se difícil raciocinar sobre seu código e, portanto, sua validade.FWIW, se você consultar os tutoriais do developer.apple.com, eles usarão a abordagem goto para tratamento de erros.
Não usamos gotos. Uma importância maior é colocada nos valores de retorno. O tratamento de exceções é feito por meio de
setjmp/longjmp
- o pouco que você puder.fonte
Não há nada moralmente errado sobre a instrução goto mais do que há algo moralmente errado com (vazio) * ponteiros.
Tudo depende de como você usa a ferramenta. No caso (trivial) que você apresentou, uma instrução case pode atingir a mesma lógica, embora com mais overhead. A verdadeira questão é: "qual é o meu requisito de velocidade?"
goto é simplesmente rápido, especialmente se você tiver o cuidado de se certificar de que compila em um salto curto. Perfeito para aplicações onde a velocidade é um prêmio. Para outros aplicativos, provavelmente faz sentido levar a sobrecarga com if / else + case para manutenção.
Lembre-se: goto não mata aplicativos, os desenvolvedores matam aplicativos.
ATUALIZAÇÃO: Aqui está o exemplo de caso
fonte
GOTO é útil. É algo que seu processador pode fazer e é por isso que você deve ter acesso a ele.
Às vezes, você deseja adicionar algo à sua função e ir para simples permite que você faça isso facilmente. Isso pode economizar tempo ..
fonte
Em geral, eu consideraria o fato de que um trecho de código poderia ser escrito de forma mais clara usando
goto
como um sintoma de que o fluxo do programa é provavelmente mais complicado do que o geralmente desejável. Combinar outras estruturas de programa de maneiras estranhas para evitar o uso degoto
tentaria tratar o sintoma, em vez da doença. Seu exemplo específico pode não ser muito difícil de implementar semgoto
:mas se a limpeza só aconteceria quando a função falhasse, o
goto
caso poderia ser tratado colocando umreturn
logo antes do primeiro rótulo de destino. O código acima requer a adição de umreturn
na linha marcada com*****
.No cenário de "limpeza mesmo em caso normal", consideraria o uso de
goto
como sendo mais claro do que os construtosdo
/while(0)
, entre outras coisas porque os próprios rótulos de destino gritam "OLHE PARA MIM" muito mais do que os construtosbreak
edo
/while(0)
. Para o caso de "limpeza apenas se houver erro", areturn
instrução acaba tendo que estar no pior lugar possível do ponto de vista da legibilidade (as instruções de retorno geralmente devem estar no início de uma função ou então no que "parece" o fim); ter umreturn
logo antes de um rótulo de destino atende a essa qualificação muito mais prontamente do que ter um logo antes do final de um "loop".BTW, um cenário onde às vezes uso
goto
para tratamento de erros é dentro de umaswitch
instrução, quando o código para vários casos compartilha o mesmo código de erro. Mesmo que meu compilador muitas vezes seja inteligente o suficiente para reconhecer que vários casos terminam com o mesmo código, acho que é mais claro dizer:Embora seja possível substituir as
goto
instruções por{handle_error(); break;}
, e embora seja possível usar umdo
/while(0)
loop junto comcontinue
para processar o pacote de execução condicional encapsulado, realmente não acho que isso seja mais claro do que usar umgoto
. Além disso, embora possa ser possível copiar o código dePACKET_ERROR
todos os lugares em quegoto PACKET_ERROR
é usado, e enquanto um compilador pode escrever o código duplicado uma vez e substituir a maioria das ocorrências com um salto para essa cópia compartilhada, o uso degoto
torna mais fácil notar os locais que configuram o pacote de maneira um pouco diferente (por exemplo, se a instrução "executar condicionalmente" decidir não executar).fonte
Eu, pessoalmente, sou um seguidor do "Poder do Dez - 10 Regras para Escrever Código de Segurança Crítica" .
Vou incluir um pequeno trecho desse texto que ilustra o que acredito ser uma boa ideia sobre goto.
Regra: Restrinja todo o código a construções de fluxo de controle muito simples - não use instruções goto, construções setjmp ou longjmp e recursão direta ou indireta.
Justificativa: Fluxo de controle mais simples se traduz em recursos mais fortes para verificação e geralmente resulta em clareza de código aprimorada. O banimento da recursão é talvez a maior surpresa aqui. Sem recursão, entretanto, temos a garantia de ter um gráfico de chamada de função acíclica, que pode ser explorado por analisadores de código e pode ajudar diretamente a provar que todas as execuções que deveriam ser limitadas são de fato limitadas. (Observe que essa regra não exige que todas as funções tenham um único ponto de retorno - embora isso também simplifique o fluxo de controle. Há casos suficientes, porém, em que um retorno de erro antecipado é a solução mais simples.)
Banir o uso de goto parece ruim, mas:
Se as regras parecem draconianas à primeira vista, tenha em mente que elas se destinam a possibilitar a verificação de códigos onde literalmente sua vida pode depender de sua correção: código que é usado para controlar o avião em que você voa, a usina nuclear a alguns quilômetros de onde você mora, ou da espaçonave que carrega astronautas para a órbita. As regras funcionam como o cinto de segurança do seu carro: no início, talvez sejam um pouco desconfortáveis, mas depois de um tempo seu uso torna-se natural e não usá-las torna-se inimaginável.
fonte
goto
é usar algum conjunto de booleanos "inteligentes" em ifs ou loops profundamente aninhados. Isso realmente não ajuda. Talvez suas ferramentas vão grocá-lo melhor, mas você não vai e você é mais importante.Concordo que a limpeza goto na ordem reversa fornecida na pergunta é a maneira mais limpa de limpar as coisas na maioria das funções. Mas também queria salientar que, às vezes, você deseja que sua função seja limpa de qualquer maneira. Nesses casos, uso a seguinte variante if if (0) {label:} idiom para ir ao ponto certo do processo de limpeza:
fonte
Parece-me que
cleanup_3
deveria fazer a sua limpeza, então liguecleanup_2
. Da mesma forma,cleanup_2
deve fazer a limpeza e, em seguida, chamar cleanup_1. Parece que sempre que você fizercleanup_[n]
isso, issocleanup_[n-1]
será necessário, portanto, deve ser responsabilidade do método (de modo que, por exemplo,cleanup_3
nunca possa ser chamado sem chamarcleanup_2
e possivelmente causar um vazamento).Dada essa abordagem, em vez de gotos, você simplesmente chamaria a rotina de limpeza e retornaria.
A
goto
abordagem não é errada ou ruim , porém, é importante notar que não é necessariamente a abordagem "mais limpa" (IMHO).Se você está procurando o desempenho ideal, suponho que a
goto
solução seja a melhor. Só espero que seja relevante, no entanto, em alguns poucos aplicativos de desempenho crítico (por exemplo, drivers de dispositivo, dispositivos incorporados, etc). Caso contrário, é uma microotimização que tem prioridade inferior à clareza do código.fonte
Eu acho que a questão aqui é falaciosa com relação ao código fornecido.
Considerar:
Portanto: do_something (), init_stuff () e prepare_stuff () devem estar fazendo sua própria limpeza . Ter uma função cleanup_1 () separada que limpa após do_something () quebra a filosofia de encapsulamento. É um design ruim.
Se eles fizeram sua própria limpeza, foo () se tornará bastante simples.
Por outro lado. Se foo () realmente criou seu próprio estado que precisava ser derrubado, então goto seria apropriado.
fonte
Aqui está o que eu preferi:
fonte
Discussão antiga, no entanto ... que tal usar "anti-padrão de seta" e encapsular mais tarde cada nível aninhado em uma função inline estática? O código parece limpo, é ideal (quando as otimizações estão habilitadas) e nenhum goto é usado. Resumindo, divida e conquiste. Abaixo um exemplo:
Em termos de espaço, estamos criando três vezes a variável na pilha, o que não é bom, mas isso desaparece ao compilar com -O2 removendo a variável da pilha e usando um registro neste exemplo simples. O que obtive do bloco acima
gcc -S -O2 test.c
foi o seguinte:fonte
Prefiro usar a técnica descrita no exemplo a seguir ...
}
fonte: http://blog.staila.com/?p=114
fonte
Usamos a
Daynix CSteps
biblioteca como outra solução para o " problema goto " nas funções init.Veja aqui e aqui .
fonte
goto