Quais são algumas das melhores maneiras de evitar o tempo de espera (0); hackear em C ++?

233

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!

Sankalp
fonte
38
RAII e lança exceções.
ta.speot.is
135
Para mim, isso parece um bom momento para usar 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 LONGO do ... while(0);parece ser a coisa errada.
quer
42
@dasblinkenlight: Sim, de fato. Se você for usar goto, seja honesto sobre isso e faça-o em campo aberto, não o esconda usando breakedo ... while
Mats Petersson
44
@MatsPetersson: Faça uma resposta, eu darei +1 para você pelo seu gotoesforço de reabilitação. :)
wilx
27
@ ta.speot.is: "RAII e lança exceções". Ao fazer isso, você estará simulando o controle de fluxo com exceções. Ou seja, é como usar hardware caro e sangrento como um martelo ou um peso de papel. Você pode fazer isso, mas isso definitivamente parece um gosto muito ruim para mim.
SigTerm 29/08

Respostas:

309

É uma prática aceitável isolar essas decisões em uma função e usar returns em vez de breaks. Embora todas essas verificações correspondam ao mesmo nível de abstração da função, é uma abordagem bastante lógica.

Por exemplo:

void foo(...)
{
   if (!condition)
   {
      return;
   }
   ...
   if (!other condition)
   {
      return;
   }
   ...
   if (!another condition)
   {
      return;
   }
   ... 
   if (!yet another condition)
   {
      return;
   }
   ...
   // Some unconditional stuff       
}
Mikhail
fonte
22
@MatsPetersson: "Isolar na função" significa refatorar para uma nova função que faz apenas os testes.
precisa saber é o seguinte
35
+1. Esta também é uma boa resposta. No C ++ 11, a função isolada pode ser uma lambda, pois também pode capturar as variáveis ​​locais, facilitando as coisas!
Nawaz 29/08
4
@deworde: Dependendo da situação, esta solução pode se tornar muito mais longa e menos legível do que o goto. Como o C ++, infelizmente, não permite definições de funções locais, você precisará mover essa função (a que você retornará) para outro lugar, o que reduz a legibilidade. Pode acabar com dezenas de parâmetros; portanto, como ter dezenas de parâmetros é uma coisa ruim, você decidirá agrupá-los em struct, o que criará um novo tipo de dados. Digitação demais para uma situação simples.
SigTerm 29/08
24
@ Damon, se houver, returné mais limpo, porque qualquer leitor imediatamente sabe que funciona corretamente e o que faz. Com gotovocê, você deve procurar ao redor para ver para que serve e ter certeza de que nenhum erro foi cometido. O disfarce é o benefício.
R. Martinho Fernandes
11
@ SigTerm: Às vezes, o código deve ser movido para uma função separada, apenas para manter o tamanho de cada função em um tamanho fácil de raciocinar. Se você não quiser pagar por uma chamada de função, marque-a forceinline.
amigos estão dizendo sobre ben
257

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 um gotocomo a break. 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:

void func()
{
   setup of lots of stuff
   ...
   if (condition)
   {
      ... 
      ...
      if (!other condition)
      {
          ...
          if (another condition)
          {
              ... 
              if (yet another condition)
              {
                  ...
                  if (...)
                     ... 
              }
          }
      }
  .... 

  }
  finish up. 
}

Na verdade, pode ficar MAIS CLARO que o código esteja correto por não ter uma lógica tão complexa.

void func()
{
   setup of lots of stuff
   ...
   if (!condition)
   {
      goto finish;
   }
   ... 
   ...
   if (other condition)
   {
      goto finish;
   }
   ...
   if (!another condition)
   {
      goto finish;
   }
   ... 
   if (!yet another condition)
   {
      goto finish;
   }
   ... 
   .... 
   if (...)
         ...    // No need to use goto here. 
 finish:
   finish up. 
}

Edit: Para esclarecer, não estou propondo o uso de gotocomo 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.

Mats Petersson
fonte
76
Discordo. gotopode ter um lugar, mas goto cleanupnão. A limpeza é feita com RAII.
precisa saber é o seguinte
14
@MSalters: Isso pressupõe que a limpeza envolve algo que pode ser resolvido com o RAII. Talvez eu devesse ter dito "erro de edição" ou algo parecido.
quer
25
Portanto, continue recebendo votos negativos sobre esse assunto, presumivelmente daqueles da crença religiosa de que gotonunca é a resposta certa. Eu apreciaria se houvesse um comentário ...
Mats Petersson
19
as pessoas odeiam gotoporque você tem que pensar em usá-lo / para entender um programa que o usa ... Por outro lado, os microprocessadores são construídos jumpse conditional jumps... então o problema é com algumas pessoas, não com lógica ou outra coisa.
Woliveirajr
17
+1 para o uso apropriado de goto. RAII não é a solução correta se erros são esperados (não excepcionais), pois isso seria um abuso de exceções.
21413 Joe
82

Você pode usar um padrão de continuação simples com uma boolvariável:

bool goOn;
if ((goOn = check0())) {
    ...
}
if (goOn && (goOn = check1())) {
    ...
}
if (goOn && (goOn = check2())) {
    ...
}
if (goOn && (goOn = check3())) {
    ...
}

Essa cadeia de execução será interrompida assim que checkNretornar a false. Nenhuma outra check...()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ção goOnde falseuma rua de sentido único, e insira o faltando goto endpara você. Como resultado, o desempenho do código acima seria idêntico ao de a do/ while(0), apenas sem um duro golpe para sua legibilidade.

dasblinkenlight
fonte
30
As atribuições dentro das ifcondições parecem muito suspeitas.
21413 Mikhail
90
@ Mikhail Apenas para um olho destreinado.
precisa saber é o seguinte
20
Eu já usei essa técnica antes e sempre me incomodava ao imaginar que o compilador teria que gerar código para verificar cada um, ifnão importando o que goOnacontecesse 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ê usar goOn &= checkN(), checkN()sempre será executado mesmo que goOnestivesse falseno início do if(ou seja, não faça isso).
marca
11
@Nawaz: Se você tem uma mente não treinada fazendo alterações arbitrárias em toda a base de código, você tem um problema muito maior do que apenas atribuições dentro de ifs.
Idelic 29/08/2013
6
@sisharp A elegância está nos olhos de quem vê! Não consigo entender como, no mundo, o uso indevido de uma construção em loop poderia ser percebido como "elegante", mas talvez seja apenas eu.
precisa saber é o seguinte
38
  1. 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.

  2. 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 breakpor return(ou throw, se for mais apropriado) e deixe o destruidor do objeto limpar para você.

  3. 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.

  4. Se você possui regras de codificação que proíbem cegamente gotoe realmente não pode simplificar o fluxo do programa, provavelmente terá que disfarçá-lo com seu dohack.

Mike Seymour
fonte
4
Humildemente, afirmo que a RAII, embora útil, não é uma bala de prata. Quando você está prestes a escrever uma classe convert-goto-para-RAII que não tem outro uso, eu definitivamente acho que você se sentiria melhor usando o idioma "goto-fim-do-mundo" mencionado pelas pessoas.
Idgy 29/08/2013
1
@busy_wait: De fato, a RAII não pode resolver tudo; é por isso que minha resposta não para com o segundo ponto, mas continua sugerindo gotose essa é realmente uma opção melhor.
Mike Seymour
3
Concordo, mas acho que é uma má idéia escrever classes de conversão goto-RAII e acho que deve ser declarado explicitamente.
Idoby
@idoby que tal escrever uma classe de modelo RAII e instancia-la com qualquer limpeza de uso único que você precisa em uma lambda
Caleth
@Caleth me parece que você está reinventando médicos / doutores?
idoby 14/02
37

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:

error_code_type c_to_refactor(result_type *r)
{
    error_code_type result = error_ok; //error_code_type/error_ok defd. elsewhere
    some_resource r1, r2; // , ...;
    if(error_ok != (result = computation1(&r1))) // Allocates local resources
        goto cleanup;
    if(error_ok != (result = computation2(&r2))) // Allocates local resources
        goto cleanup;
    // ...

    // Commit code: all operations succeeded
    *r = computed_value_n;
cleanup:
    free_resource1(r1);
    free_resource2(r2);
    return result;
}

Em C, na maioria das bases de código, o código if(error_ok != ...e gotogeralmente 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 cleanupbloco 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:

result_type cpp_code()
{
    raii_resource1 r1 = computation1();
    raii_resource2 r2 = computation2();
    // ...
    return computed_value_n;
}

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 um do { ... } while(0)no novo código, nem #defineuma macro de wrapper goto e um rótulo de limpeza, nem algo mais).

utnapistim
fonte
+1. É isso que tento usar, usando RAII ou algo assim. shared_ptrcom um deleter personalizado pode fazer muitas coisas. Ainda mais fácil com lambdas em C ++ 11.
27413 Macke
É verdade, mas se você acabar usando shared_ptr para coisas que não são ponteiros, considere pelo menos digitar : namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };neste caso (com a make_handleconfiguração do tipo de deleter correto na construção), o nome do tipo não sugere mais que é um ponteiro .
Utnapishtim
21

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á gotodisfarçado ...

earlier operations
...
[&]()->void {

    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
}();
later operations
Peter R
fonte
7
Para mim, esse hack parece pior do que o ... enquanto hack.
Michael
18

Certamente não a resposta, mas uma resposta (por uma questão de completude)

Ao invés de :

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

Você pode escrever:

switch (0) {
case 0:
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

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

#define BLOC switch(0) case 0:

E use-o como

BLOC {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

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.

kriss
fonte
2
Inteligente. Você pode até escondê-lo atrás de uma macro define BLOCK switch (0) case 0:e usá-la como BLOCK { ... break; }.
Jamesdlin #
@ JamesDlin: nunca me ocorreu antes que pudesse ser útil colocar um caso antes do suporte de abertura de um switch. Mas é realmente permitido por C e, nesse caso, é útil escrever uma boa macro.
kriss
15

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:

void main()
{
    //do stuff always
    func();
    //do other stuff always
}

void func()
{
    if (!condition)
        return;
    ...
    if (!other condition)
        return;
    ...
    if (!another condition)
        return;
    ... 
    if (!yet another condition)
        return;
    ...
}
Dan Bechard
fonte
3
Se você precisar adquirir outro recurso no meio 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.
tne
2
Não me lembro do OP dizendo nada sobre a aquisição de recursos no meio do bloco de código, mas aceito isso como um requisito viável. Nesse caso, o que há de errado em declarar o recurso na pilha em qualquer lugar func()e permitir que seu destruidor manipule a liberação de recursos? Se algo fora de func()precisar acessar o mesmo recurso, ele deve ser declarado no heap antes da chamada func()por um gerenciador de recursos adequado.
precisa saber é o seguinte
12

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.

stefaanv
fonte
11

Se você não precisar introduzir variáveis ​​locais durante a execução, geralmente poderá achatar isso:

if (check()) {
  doStuff();
}  
if (stillOk()) {
  doMoreStuff();
}
if (amIStillReallyOk()) {
  doEvenMore();
}

// edit 
doThingsAtEndAndReportErrorStatus()
the_mandrill
fonte
2
Mas então cada condição teria que incluir a anterior, que não é apenas feia, mas potencialmente prejudicial para o desempenho. Melhor pular essas verificações e limpar imediatamente assim que soubermos que "não estamos bem".
tne
Isso é verdade, no entanto, essa abordagem tem vantagens se houver coisas que precisam ser feitas no final para que você não queira retornar mais cedo (consulte a edição). Se você combinar com a abordagem de Denise de usar bools nas condições, o impacto no desempenho será insignificante, a menos que esteja em um loop muito apertado.
the_mandrill
10

Semelhante à resposta dasblinkenlight, mas evita a atribuição dentro da ifque pode ser "corrigida" por um revisor de código:

bool goOn = check0();
if (goOn) {
    ...
    goOn = check1();
}
if (goOn) {
    ...
    goOn = check2();
}
if (goOn) {
    ...
}

...

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.

Denise Skidmore
fonte
Observe que check1 () pode realmente ser PerformStep1 (), que retorna um código de resultado da etapa. Isso reduziria a complexidade da sua função de fluxo do processo.
Denise Skidmore
10

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"? .

#include <iostream>
#include <stdexcept>   // For exception, runtime_error, out_of_range

int main () {
    try {
        if (!condition)
            throw std::runtime_error("nope.");
        ...
        if (!other condition)
            throw std::runtime_error("nope again.");
        ...
        if (!another condition)
            throw std::runtime_error("told you.");
        ...
        if (!yet another condition)
            throw std::runtime_error("OK, just forget it...");
    }
    catch (std::runtime_error &e) {
        std::cout << e.what() << std::endl;
    }
    catch (...) {
        std::cout << "Caught an unknown exception\n";
    }
    return 0;
}
Cartucho
fonte
10
Realmente? Primeiro, sinceramente não vejo nenhuma melhoria na legibilidade. NÃO USE EXCEÇÕES PARA CONTROLAR O FLUXO DO PROGRAMA. Esse não é o propósito adequado. Além disso, as exceções trazem penalidades significativas de desempenho. O caso de uso adequado para uma exceção é quando existe alguma condição que NÃO PODE FAZER NADA, como tentar abrir um arquivo que já está bloqueado exclusivamente por outro processo, ou uma conexão de rede falha ou uma chamada para um banco de dados falha ou um chamador passa um parâmetro inválido para um procedimento. Esse tipo de coisas. Mas NÃO use exceções para controlar o fluxo do programa.
30813 Craig
3
Quero dizer, para não ser uma piada sobre isso, mas vá para o artigo da Stroustrup que você referenciou e procure o "Para que não devo usar exceções?" seção. Entre outras coisas, ele diz: "Em particular, o throw não é simplesmente uma maneira alternativa de retornar um valor de uma função (semelhante ao return). Isso será lento e confundirá a maioria dos programadores de C ++ que costumam ver exceções usadas apenas para erros. Da mesma forma, jogar não é uma boa maneira de sair de um loop. "
30813 Craig
3
@ Craig Tudo o que você apontou está certo, mas você está assumindo que o programa de amostra pode continuar após uma 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.
Cartucho
2
Bem, é verdade se esse é realmente o contexto. Mas, coisa engraçada sobre suposições ... ;-)
Craig
10

Para mim do{...}while(0) está bem. Se você não quiser ver do{...}while(0), pode definir palavras-chave alternativas para elas.

Exemplo:

//--------SomeUtilities.hpp---------
#define BEGIN_TEST do{
#define END_TEST }while(0);

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) break;
   if(!condition2) break;
   if(!condition3) break;
   if(!condition4) break;
   if(!condition5) break;

   //processing code here

END_TEST

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 gototambém produz um código mais limpo e é direto com a lógica de condição e salto. Você pode fazer o seguinte:

{
   if(!condition1) goto end_blahblah;
   if(!condition2) goto end_blahblah;
   if(!condition3) goto end_blahblah;
   if(!condition4) goto end_blahblah;
   if(!condition5) goto end_blahblah;

   //processing code here

 }end_blah_blah:;  //use appropriate label here to describe...
                   //  ...the whole code inside the block.

Observe que a etiqueta é colocada após o fechamento }. Este é o problema gotoque evita um possível: colocar um código acidentalmente no meio porque você não viu o rótulo. Agora é como do{...}while(0)sem código de condição.

Para tornar esse código mais limpo e compreensível, você pode fazer o seguinte:

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);
   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);
   if(!condition5) FAILED(NormalizeData);

END_TEST(NormalizeData)

Com isso, você pode criar blocos aninhados e especificar onde deseja sair / sair.

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);

   BEGIN_TEST
      if(!conditionAA) FAILED(DecryptBlah);
      if(!conditionBB) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionCC) FAILED(DecryptBlah);

      // --We can now decrypt and do other stuffs.

   END_TEST(DecryptBlah)

   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);

   // --other code here

   BEGIN_TEST
      if(!conditionA) FAILED(TrimSpaces);
      if(!conditionB) FAILED(TrimSpaces);
      if(!conditionC) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionD) FAILED(TrimSpaces);

      // --We can now trim completely or do other stuffs.

   END_TEST(TrimSpaces)

   // --Other code here...

   if(!condition5) FAILED(NormalizeData);

   //Ok, we got here. We can now process what we need to process.

END_TEST(NormalizeData)

O código do espaguete não é culpa de goto, é culpa do programador. Você ainda pode produzir código de espaguete sem usar goto.

acegs
fonte
10
Eu escolheria gotoestender a sintaxe da linguagem usando o pré-processador um milhão de vezes.
Christian
2
"Para tornar esse código mais limpo e compreensível, você pode [usar LOADS_OF_WEIRD_MACROS]" : não calcula.
underscore_d
8

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:

boost::optional<EnabledContext> enabled(boost::optional<Context> context);
boost::optional<EnergisedContext> energised(boost::optional<EnabledContext> context);

Cada etapa computacional pode obviamente fazer algo como retornar boost::nonese o opcional que foi fornecido estiver vazio. Então, por exemplo:

struct Context { std::string coordinates_filename; /* ... */ };

struct EnabledContext { int x; int y; int z; /* ... */ };

boost::optional<EnabledContext> enabled(boost::optional<Context> c) {
   if (!c) return boost::none; // this line becomes implicit if going the whole hog with monads
   if (!exists((*c).coordinates_filename)) return boost::none; // return none when any error is encountered.
   EnabledContext ec;
   std::ifstream file_in((*c).coordinates_filename.c_str());
   file_in >> ec.x >> ec.y >> ec.z;
   return boost::optional<EnabledContext>(ec); // All ok. Return non-empty value.
}

Em seguida, encadeie-os:

Context context("planet_surface.txt", ...); // Close over all needed bits and pieces

boost::optional<EnergisedContext> result(energised(enabled(context)));
if (result) { // A single level "if" statement
    // do work on *result
} else {
    // error
}

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.

Bento
fonte
3
Esse código possui uma propriedade indesejada de forçar cada uma das funções individuais a lidar com a falha da função anterior, não utilizando assim adequadamente o idioma Monad (onde efeitos monádicos, nesse caso, falha, devem ser tratados implicitamente). Para fazer isso, você precisa ter em 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.
Rotsor 30/08/2013
Obrigado. Você está correto - essa é a maneira de fazê-lo corretamente. Eu não queria escrever muito em minha resposta para explicar isso (daí o termo "homem pobre", que deveria sugerir que eu não estava indo bem aqui).
Benedict
7

Que tal mover as instruções if para uma função extra que produza um resultado numérico ou enum?

int ConditionCode (void) {
   if (condition1)
      return 1;
   if (condition2)
      return 2;
   ...
   return 0;
}


void MyFunc (void) {
   switch (ConditionCode ()) {
      case 1:
         ...
         break;

      case 2:
         ...
         break;

      ...

      default:
         ...
         break;
   }
}
karx11erx
fonte
Isso é bom quando possível, mas muito menos geral do que a pergunta feita aqui. Todas as condições podem depender do código executado após o último teste de depuração.
kriss
O problema aqui é que você separa causa e consequência. Ou seja, você separa o código que se refere ao mesmo número de condição e isso pode ser uma fonte de erros adicionais.
Riga
@kriss: Bem, a função ConditionCode () pode ser ajustada para cuidar disso. O ponto principal dessa função é que você pode usar o retorno <resultado> para uma saída limpa assim que uma condição final for calculada; E é isso que está fornecendo clareza estrutural aqui.
precisa saber é o seguinte
@Riga: Imo, essas são objeções completamente acadêmicas. Acho que o C ++ se torna mais complexo, enigmático e ilegível a cada nova versão. Não vejo problema com uma pequena função auxiliar avaliando condições complexas de uma maneira bem estruturada para tornar a função de destino logo abaixo mais legível.
precisa saber é o seguinte
1
@ karx11erx minhas objeções são práticas e baseadas na minha experiência. Esse padrão é ruim independentemente do C ++ 11 ou de qualquer outro idioma. Se você tiver dificuldades com construções de linguagem que permitem escrever uma boa arquitetura que não seja um problema de linguagem.
Riga
5

Algo assim talvez

#define EVER ;;

for(EVER)
{
    if(!check()) break;
}

ou use exceções

try
{
    for(;;)
        if(!check()) throw 1;
}
catch()
{
}

Usando exceções, você também pode transmitir dados.

liftarn
fonte
10
Por favor, não faça coisas inteligentes como a sua definição de sempre, pois elas geralmente tornam o código mais difícil de ler para outros desenvolvedores. Eu vi alguém definindo Case como break; case em um arquivo de cabeçalho e o usei em um switch em um arquivo cpp, fazendo com que outros se perguntassem por horas por que o switch quebra entre as instruções Case. Grrr ...
Michael
5
E quando você nomear macros, faça com que pareçam macros (ou seja, em maiúsculas). Caso contrário, alguém que nomeie uma variável / função / tipo / etc. chamada everserá muito infeliz ...
jamesdlin
5

Não estou particularmente interessado em usar breakoureturn 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,

if (conditionA) {
    ....
    if (conditionB) {
        ....
        if (conditionC) {
            myLogic();
        }
    }
}

Observando os blocos anexos, é fácil descobrir que isso myLogic()só acontece quando conditionA and conditionB and conditionCé verdade.

Torna-se muito menos visível quando há retornos antecipados:

if (conditionA) {
    ....
    if (!conditionB) {
        return;
    }
    if (!conditionD) {
        return;
    }
    if (conditionC) {
        myLogic();
    }
}

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:

if (conditionA) {
    isA = true;
    ....
}

if (isA && conditionB) {
    isB = true;
    ...
}

if (isB && conditionC) {
    isC = true;
    myLogic();
}

(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 quando isB && conditionC. O leitor recebe uma dica de que precisa pesquisar mais o que fará com que B seja verdadeiro.

Adrian Shum
fonte
3
typedef bool (*Checker)();

Checker * checkers[]={
 &checker0,&checker1,.....,&checkerN,NULL
};

bool checker1(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

bool checker2(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

......

void doCheck(){
  Checker ** checker = checkers;
  while( *checker && (*checker)())
    checker++;
}

Que tal isso?

sniperbat
fonte
o if é obsoleto, apenas return condition;, caso contrário, acho que isso é bem sustentável.
SpaceTrucker 5/09/13
2

Outro padrão útil se você precisar de diferentes etapas de limpeza, dependendo de onde está a falha:

    private ResultCode DoEverything()
    {
        ResultCode processResult = ResultCode.FAILURE;
        if (DoStep1() != ResultCode.SUCCESSFUL)
        {
            Step1FailureCleanup();
        }
        else if (DoStep2() != ResultCode.SUCCESSFUL)
        {
            Step2FailureCleanup();
            processResult = ResultCode.SPECIFIC_FAILURE;
        }
        else if (DoStep3() != ResultCode.SUCCESSFUL)
        {
            Step3FailureCleanup();
        }
        ...
        else
        {
            processResult = ResultCode.SUCCESSFUL;
        }
        return processResult;
    }
Denise Skidmore
fonte
2

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.

SpaceTrucker
fonte
Você poderia adicionar um exemplo em um idioma diferente? Penso que esta pergunta se aplica a muitas línguas, embora tenha sido perguntada especificamente sobre C ++.
Denise Skidmore
1

É assim que eu faço.

void func() {
  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...
}
Vadoff
fonte
1

Primeiro, um pequeno exemplo para mostrar por que gotonão é uma boa solução para C ++:

struct Bar {
    Bar();
};

extern bool check();

void foo()
{
    if (!check())
       goto out;

    Bar x;

    out:
}

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:

bool calc1()
{
    if (!check())
        return false;

    // ... Do stuff1 here ...

    if (!calc2()) {
        // ... Undo stuff1 here ...
        return false;
    }

    return true;
}

bool calc2()
{
    if (!check())
        return false;

    // ... Do stuff2 here ...

    if (!calc3()) {
        // ... Undo stuff2 here ...
        return false;
    }

    return true;
}

...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 calcfunçõ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.)

Nemo
fonte
É muito fácil auditar o caminho da limpeza, mas talvez não seja tão fácil rastrear o caminho dourado. Mas, no geral, acho que algo assim encoraja um padrão de limpeza consistente.
Denise Skidmore
0

Se você codificar com um bloco longo de instruções if..else if..else, tente reescrever o bloco inteiro com a ajuda de Functorsou function pointers. Pode não ser a solução certa sempre, mas muitas vezes é.

http://www.cprogramming.com/tutorial/functors-function-objects-in-c++.html

HAL
fonte
Em princípio, isso é possível (e não é ruim), mas com objetos de função explícitos ou -pointers, apenas interrompe o fluxo de código com muita força. OTOH, aqui equivalente, o uso de lambdas ou funções nomeadas comuns é uma boa prática, eficiente e com boa leitura.
usar o seguinte código
0

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:

do {

    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

finishingUpStuff.

Agora:

finish(params)
{
  ...
  ...
}

if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...

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.

Sankalp
fonte
0

Consolide-o em uma ifdeclaração:

if(
    condition
    && other_condition
    && another_condition
    && yet_another_condition
    && ...
) {
        if (final_cond){
            //Do stuff
        } else {
            //Do other stuff
        }
}

Esse é o padrão usado em linguagens como Java, onde a palavra-chave goto foi removida.

Tyzoid
fonte
2
Isso só funciona se você não precisar fazer nada entre os testes de condição. (bem, eu suponho que você poderia esconder a fazer-de-coisas dentro de algumas chamadas de função feitas pela condição-testes, mas isso pode ficar um pouco ofuscado se você fez isso muito)
Jeremy Friesner
@JeremyFriesner Na verdade, você pode realmente fazer as coisas intermediárias como funções booleanas separadas que sempre avaliam como 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.
precisa saber é o seguinte
@AJMansfield sim, é a isso que eu estava me referindo na segunda frase ... mas não tenho certeza se isso seria uma melhoria na qualidade do código.
Jeremy Friesner
@JeremyFriesner Nada impede você de escrever as condições (/*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 que gotodeclaração ...
cmaster - Reintegrar monica
@ JeremyFriesner Eu estava assumindo que estes eram booleanos. Se funções foram executadas dentro de cada condição, existe uma maneira melhor de lidar com isso.
Tyzoid
0

Se você estiver usando o mesmo manipulador de erros para todos os erros, e cada etapa retornará um bool indicando sucesso:

if(
    DoSomething() &&
    DoSomethingElse() &&
    DoAThirdThing() )
{
    // do good condition action
}
else
{
    // handle error
}

(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.)

Denise Skidmore
fonte
0

Por que o método de sinalização não foi respondido, ele é usado desde as idades.

//you can use something like this (pseudocode)
long var = 0;
if(condition)  flag a bit in var
if(condition)  flag another bit in var
if(condition)  flag another bit in var
............
if(var == certain number) {
Do the required task
}
Fennekin
fonte