Quando o fluxo de código é assim:
if(check())
{
...
...
if(check())
{
...
...
if(check())
{
...
...
}
}
}
Eu geralmente vi esse trabalho para evitar o fluxo de código desarrumado acima:
do {
if(!check()) break;
...
...
if(!check()) break;
...
...
if(!check()) break;
...
...
} while(0);
Quais são algumas maneiras melhores de evitar essa solução alternativa / hackear para que ela se torne um código de nível superior (nível da indústria)?
Quaisquer sugestões prontas para uso são bem-vindas!
goto
- mas tenho certeza de que alguém me marcará por sugerir isso, então não estou escrevendo uma resposta para esse efeito. Ter um LONGOdo ... while(0);
parece ser a coisa errada.goto
, seja honesto sobre isso e faça-o em campo aberto, não o esconda usandobreak
edo ... while
goto
esforço de reabilitação. :)Respostas:
É uma prática aceitável isolar essas decisões em uma função e usar
return
s em vez debreak
s. Embora todas essas verificações correspondam ao mesmo nível de abstração da função, é uma abordagem bastante lógica.Por exemplo:
fonte
return
é mais limpo, porque qualquer leitor imediatamente sabe que funciona corretamente e o que faz. Comgoto
você, você deve procurar ao redor para ver para que serve e ter certeza de que nenhum erro foi cometido. O disfarce é o benefício.forceinline
.Há momentos em que o uso
goto
é realmente a resposta certa - pelo menos para aqueles que não são educados na crença religiosa de que "goto
nunca pode ser a resposta, não importa qual seja a pergunta" - e esse é um desses casos.Este código está usando o hack de
do { ... } while(0);
com o único objetivo de vestir umgoto
como abreak
. Se você vai usargoto
, seja aberto sobre isso. Não faz sentido tornar o código MAIS DIFÍCIL de ler.Uma situação específica é justamente quando você tem muito código com condições bastante complexas:
Na verdade, pode ficar MAIS CLARO que o código esteja correto por não ter uma lógica tão complexa.
Edit: Para esclarecer, não estou propondo o uso de
goto
como uma solução geral. Mas há casos em quegoto
há uma solução melhor do que outras soluções.Imagine, por exemplo, que estamos coletando alguns dados, e as diferentes condições que estão sendo testadas são uma espécie de "este é o fim dos dados que estão sendo coletados" - o que depende de algum tipo de marcadores "continuar / terminar" que variam dependendo de onde você está no fluxo de dados.
Agora, quando terminarmos, precisamos salvar os dados em um arquivo.
E sim, muitas vezes existem outras soluções que podem fornecer uma solução razoável, mas nem sempre.
fonte
goto
pode ter um lugar, masgoto cleanup
não. A limpeza é feita com RAII.goto
nunca é a resposta certa. Eu apreciaria se houvesse um comentário ...goto
porque você tem que pensar em usá-lo / para entender um programa que o usa ... Por outro lado, os microprocessadores são construídosjumps
econditional jumps
... então o problema é com algumas pessoas, não com lógica ou outra coisa.goto
. RAII não é a solução correta se erros são esperados (não excepcionais), pois isso seria um abuso de exceções.Você pode usar um padrão de continuação simples com uma
bool
variável:Essa cadeia de execução será interrompida assim que
checkN
retornar afalse
. Nenhuma outracheck...()
chamada seria realizada devido a curto-circuito do&&
operador. Além disso, os compiladores de otimização são suficientes inteligente para reconhecer que a fixaçãogoOn
defalse
uma rua de sentido único, e insira o faltandogoto end
para você. Como resultado, o desempenho do código acima seria idêntico ao de ado
/while(0)
, apenas sem um duro golpe para sua legibilidade.fonte
if
condições parecem muito suspeitas.if
não importando o quegoOn
acontecesse mesmo se um dos primeiros falhasse (em vez de pular / sair) ... mas acabei de executar um teste e o VS2012 pelo menos era inteligente o suficiente para causar um curto-circuito em tudo após o primeiro falso de qualquer maneira. Vou usar isso com mais frequência. nota: se você usargoOn &= checkN()
,checkN()
sempre será executado mesmo quegoOn
estivessefalse
no início doif
(ou seja, não faça isso).if
s.Tente extrair o código em uma função separada (ou talvez mais de uma). Em seguida, retorne da função se a verificação falhar.
Se estiver muito acoplado ao código ao redor para fazer isso, e você não conseguir encontrar uma maneira de reduzir o acoplamento, observe o código após este bloco. Presumivelmente, ele limpa alguns recursos usados pela função. Tente gerenciar esses recursos usando um objeto RAII ; então substitua cada desonesto
break
porreturn
(outhrow
, se for mais apropriado) e deixe o destruidor do objeto limpar para você.Se o fluxo do programa é (necessariamente) tão irregular que você realmente precisa de um
goto
, use-o em vez de disfarçar um estranho.Se você possui regras de codificação que proíbem cegamente
goto
e realmente não pode simplificar o fluxo do programa, provavelmente terá que disfarçá-lo com seudo
hack.fonte
goto
se essa é realmente uma opção melhor.TLDR : RAII , código transacional (somente defina resultados ou retorne itens quando já estiver computado) e exceções.
Resposta longa:
Em C , a melhor prática para esse tipo de código é adicionar um rótulo EXIT / CLEANUP / other no código, onde ocorre a limpeza dos recursos locais e um código de erro (se houver) é retornado. Essa é uma prática recomendada porque divide o código naturalmente em inicialização, computação, confirmação e retorno:
Em C, na maioria das bases de código, o código
if(error_ok != ...
egoto
geralmente está oculto atrás de algumas macros de conveniência (RET(computation_result)
,ENSURE_SUCCESS(computation_result, return_code)
, etc.).C ++ oferece ferramentas extras sobre C :
A funcionalidade do bloco de limpeza pode ser implementada como RAII, o que significa que você não precisa mais do
cleanup
bloco inteiro e permite que o código do cliente adicione instruções de retorno antecipado.Você joga sempre que não pode continuar, transformando tudo
if(error_ok != ...
em chamadas diretas.Código C ++ equivalente:
Esta é uma prática recomendada porque:
É explícito (ou seja, enquanto o tratamento de erros não é explícito, o fluxo principal do algoritmo é)
É simples escrever código do cliente
É mínimo
É simples
Não possui construções de código repetitivo
Ele não usa macros
Ele não usa
do { ... } while(0)
construções estranhasÉ reutilizável com o mínimo de esforço (ou seja, se eu quiser copiar a chamada para
computation2();
uma função diferente, não preciso me certificar de adicionar umdo { ... } while(0)
no novo código, nem#define
uma macro de wrapper goto e um rótulo de limpeza, nem algo mais).fonte
shared_ptr
com um deleter personalizado pode fazer muitas coisas. Ainda mais fácil com lambdas em C ++ 11.namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };
neste caso (com amake_handle
configuração do tipo de deleter correto na construção), o nome do tipo não sugere mais que é um ponteiro .Estou adicionando uma resposta por uma questão de integridade. Várias outras respostas apontaram que o grande bloco de condições poderia ser dividido em uma função separada. Mas, como também foi apontado várias vezes, essa abordagem separa o código condicional do contexto original. Esse é um dos motivos pelos quais lambdas foram adicionadas ao idioma no C ++ 11. O uso de lambdas foi sugerido por outros, mas nenhuma amostra explícita foi fornecida. Eu coloquei um nesta resposta. O que me impressiona é que ele se parece muito com a
do { } while(0)
abordagem de várias maneiras - e talvez isso signifique que ainda estágoto
disfarçado ...fonte
Certamente não a resposta, mas uma resposta (por uma questão de completude)
Ao invés de :
Você pode escrever:
Isso ainda é um pulo disfarçado, mas pelo menos não é mais um loop. Que significa que você não terá que verificar com muito cuidado que não há algum continuar em algum lugar escondido no bloco.
A construção também é simples o suficiente para que você possa esperar que o compilador a otimize.
Conforme sugerido por @jamesdlin, você pode até esconder isso atrás de uma macro como
E use-o como
Isso é possível porque a sintaxe da linguagem C espera uma instrução após uma troca, não um bloco entre colchetes e você pode colocar um rótulo de caso antes dessa instrução. Até agora, não via o ponto de permitir isso, mas nesse caso em particular é útil ocultar o comutador atrás de uma macro agradável.
fonte
define BLOCK switch (0) case 0:
e usá-la comoBLOCK { ... break; }
.Eu recomendaria uma abordagem semelhante à Mats responder menos o desnecessário
goto
. Coloque apenas a lógica condicional na função. Qualquer código que sempre é executado deve ir antes ou depois da função ser chamada no chamador:fonte
func
, precisará fatorar outra função (de acordo com seu padrão). Se todas essas funções isoladas precisarem dos mesmos dados, você acabará copiando os mesmos argumentos da pilha repetidamente ou decidirá alocar seus argumentos na pilha e passar um ponteiro, sem aproveitar o recurso mais básico da linguagem ( argumentos da função). Em resumo, não acredito que essa solução seja dimensionada para o pior caso em que todas as condições sejam verificadas após a aquisição de novos recursos que devem ser limpos. Nota Nawaz comentar sobre lambdas no entanto.func()
e permitir que seu destruidor manipule a liberação de recursos? Se algo fora defunc()
precisar acessar o mesmo recurso, ele deve ser declarado no heap antes da chamadafunc()
por um gerenciador de recursos adequado.O fluxo de código em si já é um cheiro de código que muito está acontecendo na função. Se não houver uma solução direta para isso (a função é uma função de verificação geral), usar RAII para que você possa retornar em vez de pular para a seção final da função pode ser melhor.
fonte
Se você não precisar introduzir variáveis locais durante a execução, geralmente poderá achatar isso:
fonte
Semelhante à resposta dasblinkenlight, mas evita a atribuição dentro da
if
que pode ser "corrigida" por um revisor de código:...
Uso esse padrão quando os resultados de uma etapa precisam ser verificados antes da próxima etapa, o que difere de uma situação em que todas as verificações podem ser feitas antecipadamente com um grande
if( check1() && check2()...
padrão de tipo.fonte
Use exceções. Seu código ficará muito mais limpo (e as exceções foram criadas exatamente para lidar com erros no fluxo de execução de um programa). Para limpar recursos (descritores de arquivo, conexões com o banco de dados, etc.), leia o artigo Por que o C ++ não fornece uma construção "finalmente"? .
fonte
check()
falha de condição, e essa é certamente a SUA suposição, não há contexto na amostra. Supondo que o programa não possa continuar, o uso de exceções é o caminho a percorrer.Para mim
do{...}while(0)
está bem. Se você não quiser verdo{...}while(0)
, pode definir palavras-chave alternativas para elas.Exemplo:
Eu acho que o compilador irá remover o desnecessário
while(0)
condição emdo{...}while(0)
na versão binária e converterá as quebras em salto incondicional. Você pode verificar a versão da linguagem assembly para ter certeza.O uso
goto
também produz um código mais limpo e é direto com a lógica de condição e salto. Você pode fazer o seguinte:Observe que a etiqueta é colocada após o fechamento
}
. Este é o problemagoto
que evita um possível: colocar um código acidentalmente no meio porque você não viu o rótulo. Agora é comodo{...}while(0)
sem código de condição.Para tornar esse código mais limpo e compreensível, você pode fazer o seguinte:
Com isso, você pode criar blocos aninhados e especificar onde deseja sair / sair.
O código do espaguete não é culpa de
goto
, é culpa do programador. Você ainda pode produzir código de espaguete sem usargoto
.fonte
goto
estender a sintaxe da linguagem usando o pré-processador um milhão de vezes.Este é um problema conhecido e bem resolvido de uma perspectiva de programação funcional - talvez a mônada.
Em resposta ao comentário que recebi abaixo, editei minha introdução aqui: Você pode encontrar detalhes completos sobre a implementação de mônadas C ++ em vários locais, o que permitirá que você consiga o que o Rotsor sugere. Demora um pouco para grungar mônadas, então, em vez disso, vou sugerir aqui um rápido mecanismo de mônada "pobre-homem", para o qual você precisa saber nada além de boost :: optional.
Configure suas etapas de computação da seguinte maneira:
Cada etapa computacional pode obviamente fazer algo como retornar
boost::none
se o opcional que foi fornecido estiver vazio. Então, por exemplo:Em seguida, encadeie-os:
O bom disso é que você pode escrever testes de unidade claramente definidos para cada etapa computacional. Além disso, a invocação é lida como inglês simples (como é geralmente o caso do estilo funcional).
Se você não se importa com a imutabilidade e é mais conveniente retornar o mesmo objeto sempre que surgir alguma variação usando shared_ptr ou algo semelhante.
fonte
optional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);
vez disso e usar a operação de composição monádica ('bind') em vez da aplicação de função.Que tal mover as instruções if para uma função extra que produza um resultado numérico ou enum?
fonte
Algo assim talvez
ou use exceções
Usando exceções, você também pode transmitir dados.
fonte
ever
será muito infeliz ...Não estou particularmente interessado em usar
break
oureturn
nesse caso. Dado que, normalmente, quando estamos diante de uma situação dessas, geralmente é um método relativamente longo.Se estivermos tendo vários pontos de saída, pode haver dificuldades quando queremos saber o que fará com que determinada lógica seja executada: Normalmente, continuamos subindo os blocos que encerram essa parte da lógica, e os critérios desses blocos nos dizem o seguinte: situação:
Por exemplo,
Observando os blocos anexos, é fácil descobrir que isso
myLogic()
só acontece quandoconditionA and conditionB and conditionC
é verdade.Torna-se muito menos visível quando há retornos antecipados:
Não podemos mais navegar de lá
myLogic()
, olhando o bloco anexo para descobrir a condição.Existem soluções alternativas diferentes que eu usei. Aqui está um deles:
(Obviamente, é recomendável usar a mesma variável para substituir todos os
isA isB isC
.)Tal abordagem pelo menos dará ao leitor de código, que
myLogic()
é executado quandoisB && conditionC
. O leitor recebe uma dica de que precisa pesquisar mais o que fará com que B seja verdadeiro.fonte
Que tal isso?
fonte
return condition;
, caso contrário, acho que isso é bem sustentável.Outro padrão útil se você precisar de diferentes etapas de limpeza, dependendo de onde está a falha:
fonte
Eu não sou um C ++ programador de , então não escreverei nenhum código aqui, mas até agora ninguém mencionou uma solução orientada a objetos. Então, aqui está o meu palpite sobre isso:
Tenha uma interface genérica que forneça um método para avaliar uma única condição. Agora você pode usar uma lista de implementações dessas condições em seu objeto, contendo o método em questão. Você percorre a lista e avalia cada condição, possivelmente iniciando cedo se uma falhar.
O bom é que esse design adere muito bem ao princípio de abertura / fechamento , porque é possível adicionar facilmente novas condições durante a inicialização do objeto que contém o método em questão. Você pode até adicionar um segundo método à interface com o método de avaliação da condição, retornando uma descrição da condição. Isso pode ser usado para sistemas de auto-documentação.
A desvantagem, no entanto, é que há um pouco mais de sobrecarga envolvida devido ao uso de mais objetos e à iteração na lista.
fonte
É assim que eu faço.
fonte
Primeiro, um pequeno exemplo para mostrar por que
goto
não é uma boa solução para C ++:Tente compilar isso em um arquivo de objeto e veja o que acontece. Em seguida, tente o equivalente
do
+break
+while(0)
.Isso foi um aparte. O ponto principal segue.
Esses pequenos pedaços de código geralmente exigem algum tipo de limpeza, caso a função inteira falhe. Essas limpezas geralmente querem acontecer na ordem oposta às dos próprios pedaços, à medida que você "desenrola" o cálculo parcialmente finalizado.
Uma opção para obter essas semânticas é a RAII ; veja a resposta de @ utnapistim. O C ++ garante que os destruidores automáticos sejam executados na ordem oposta aos construtores, o que naturalmente fornece um "desenrolamento".
Mas isso requer muitas classes RAII. Às vezes, uma opção mais simples é usar a pilha:
...e assim por diante. Isso é fácil de auditar, pois coloca o código "desfazer" ao lado do código "do". Auditoria fácil é boa. Também torna o fluxo de controle muito claro. Também é um padrão útil para C.
Pode exigir que as
calc
funções recebam muitos argumentos, mas isso geralmente não é um problema se suas classes / estruturas tiverem uma boa coesão. (Ou seja, as coisas que pertencem juntas vivem em um único objeto; portanto, essas funções podem levar ponteiros ou referências a um pequeno número de objetos e ainda fazer muito trabalho útil.)fonte
Se você codificar com um bloco longo de instruções if..else if..else, tente reescrever o bloco inteiro com a ajuda de
Functors
oufunction pointers
. Pode não ser a solução certa sempre, mas muitas vezes é.http://www.cprogramming.com/tutorial/functors-function-objects-in-c++.html
fonte
Estou impressionado com o número de respostas diferentes apresentadas aqui. Mas, finalmente, no código que tenho que alterar (ou seja, remover esse
do-while(0)
hack ou algo assim), fiz algo diferente de qualquer uma das respostas mencionadas aqui e estou confuso por que ninguém pensou nisso. Aqui está o que eu fiz:Código inicial:
Agora:
Então, o que foi feito aqui é que o material de acabamento foi isolado em uma função e, de repente, as coisas se tornaram tão simples e limpas!
Eu pensei que essa solução valia a pena mencionar, então a forneceu aqui.
fonte
Consolide-o em uma
if
declaração:Esse é o padrão usado em linguagens como Java, onde a palavra-chave goto foi removida.
fonte
true
. A avaliação de curto-circuito garantiria que você nunca execute itens intermediários para os quais todos os testes de pré-requisito não foram aprovados.(/*do other stuff*/, /*next condition*/)
, pois você pode até formatá-lo bem. Só não espere que as pessoas gostem. Mas, honestamente, isso só serve para mostrar que era um erro para Java para a fase quegoto
declaração ...Se você estiver usando o mesmo manipulador de erros para todos os erros, e cada etapa retornará um bool indicando sucesso:
(Semelhante à resposta da tyzoid, mas as condições são as ações e o && impede que outras ações ocorram após a primeira falha.)
fonte
Por que o método de sinalização não foi respondido, ele é usado desde as idades.
fonte