Como evitar correntes "se"?

266

Supondo que eu tenho esse pseudo-código:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();

As funções executeStepXdevem ser executadas se e somente se as anteriores tiverem êxito. De qualquer forma, a executeThisFunctionInAnyCasefunção deve ser chamada no final. Sou iniciante em programação, desculpe-me pela pergunta muito básica: existe uma maneira (em C / C ++, por exemplo) de evitar tanto tempo?if cadeia produzindo esse tipo de "pirâmide de código", às custas da legibilidade do código ?

Eu sei que se pudéssemos pular a executeThisFunctionInAnyCasechamada de função, o código poderia ser simplificado como:

bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Mas a restrição é a executeThisFunctionInAnyCasechamada de função. A breakdeclaração poderia ser usada de alguma maneira?

ABCplus
fonte
254
@ FrédéricHamidi errado errado errado! Nunca diga que dirigir o fluxo do programa com exceções é bom! Definitivamente, as exceções NÃO são adequadas para esse fim, por muitas razões.
Piotr Zierhoffer
26
@ Piotr, fui mimado pelo Python (o que realmente incentiva isso). Eu sei que as exceções não devem ser usadas para controle de fluxo em C ++, mas é realmente controle de fluxo aqui? Uma função que retorna não poderia falseser considerada semelhante a uma situação excepcional?
Frédéric Hamidi
13
Isso depende da semântica de um programa. Um falseretorno pode ser bastante normal.
dornhege
28
Retrocedi sua pergunta para a primeira revisão. Você não deve alterar sua pergunta radicalmente depois de receber um certo número de perguntas (> 0), porque isso invalidará todas as respostas dadas até aquele momento e criará confusão. Abra uma nova pergunta.
26614 Shoe
13
Eu gostaria que todos os “programadores novatos” fizessem perguntas de design como esta.
precisa

Respostas:

486

Você pode usar um &&(AND lógico):

if (executeStepA() && executeStepB() && executeStepC()){
    ...
}
executeThisFunctionInAnyCase();

isso atenderá a ambos os seus requisitos:

  • executeStep<X>()deve avaliar apenas se o anterior foi bem-sucedido (isso é chamado de avaliação de curto-circuito )
  • executeThisFunctionInAnyCase() será executado em qualquer caso
Sapato
fonte
29
Isso é semanticamente correto (de fato, queremos que TODAS as condições sejam verdadeiras), além de uma técnica de programação muito boa (avaliação de curto-circuito). Além disso, isso pode ser usado em qualquer situação complexa em que a criação de funções atrapalhe o código.
Sanchises
58
@RobAu: Então será bom para os programadores juniores finalmente verem o código que se baseia na avaliação de atalho e podem levá-los a reavaliar esse tópico que, por sua vez, os ajudaria a se tornar programadores seniores. Tão claramente um ganha-ganha: código decente e alguém aprendeu algo ao lê-lo.
X4u
24
Essa deve ser a resposta principal
Nome para exibição
61
@ RobAu: Este não é um truque tirando proveito de algum truque obscuro de sintaxe, é altamente idiomático em quase todas as linguagens de programação, a ponto de ser uma prática imbatível no padrão.
BlueRaja - Danny Pflughoeft
38
Esta solução só se aplica se as condições forem realmente chamadas de função simples. No código real, essas condições podem ter de 2 a 5 linhas (e são combinações de muitas outras ) &&e, ||portanto, não há como você querer uni-las em uma única ifinstrução sem estragar a legibilidade. E nem sempre é fácil mover essas condições para funções externas, porque elas podem depender de muitas variáveis ​​locais previamente calculadas, o que criaria uma terrível bagunça se você tentar passar cada uma delas como argumento individual.
precisa
358

Basta usar uma função adicional para que sua segunda versão funcione:

void foo()
{
  bool conditionA = executeStepA();
  if (!conditionA) return;

  bool conditionB = executeStepB();
  if (!conditionB) return;

  bool conditionC = executeStepC();
  if (!conditionC) return;
}

void bar()
{
  foo();
  executeThisFunctionInAnyCase();
}

Usar ifs profundamente aninhados (sua primeira variante) ou o desejo de interromper "parte de uma função" geralmente significa que você precisa de uma função extra.

ltjax
fonte
51
+1 finalmente alguém postou uma resposta sensata. Esta é a maneira mais correta, segura e legível, na minha opinião.
precisa
31
+1 Esta é uma boa ilustração do "Princípio da responsabilidade única". A função foofunciona através de uma sequência de condições e ações relacionadas. A função baré claramente separada das decisões. Se vimos os detalhes das condições e ações, pode ser que fooainda esteja fazendo muito, mas por enquanto essa é uma boa solução.
GraniteRobert
13
A desvantagem é que C não possui funções aninhadas, portanto, se essas três etapas necessárias para usar variáveis ​​de barvocê precisarem passá-las manualmente foocomo parâmetros. Se for esse o caso e se foofor chamado apenas uma vez, eu iria errar no uso da versão goto para evitar definir duas funções fortemente acopladas que acabariam não sendo muito reutilizáveis.
hugomg
7
Não tenho certeza da sintaxe-circuito curto para C, mas em C # foo () pode ser escrito comoif (!executeStepA() || !executeStepB() || !executeStepC()) return
Travis
6
@ user1598390 Os Goto são usados ​​o tempo todo, especialmente na programação de sistemas quando você precisa desenrolar muito código de configuração.
Scotty Bauer
166

Os programadores da velha escola C usam gotoneste caso. É o único uso gotoincentivado pelo guia de estilo do Linux, chamado de saída de função centralizada:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanup;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    executeThisFunctionInAnyCase();
    return result;
}

Algumas pessoas trabalham em torno do uso gotoenvolvendo o corpo em um loop e quebrando-o, mas efetivamente as duas abordagens fazem a mesma coisa. A gotoabordagem é melhor se você precisar de alguma outra limpeza somente se executeStepA()for bem-sucedida:

int foo() {
    int result = /*some error code*/;
    if(!executeStepA()) goto cleanupPart;
    if(!executeStepB()) goto cleanup;
    if(!executeStepC()) goto cleanup;

    result = 0;
cleanup:
    innerCleanup();
cleanupPart:
    executeThisFunctionInAnyCase();
    return result;
}

Com a abordagem de loop, você acabaria com dois níveis de loops nesse caso.

cmaster - restabelece monica
fonte
108
+1. Muitas pessoas veem um gotoe imediatamente pensam "Este é um código terrível", mas ele tem seus usos válidos.
Graeme Perrow
46
Exceto que esse código é realmente bastante confuso, do ponto de vista da manutenção. Especialmente quando existem vários rótulos, nos quais o código também fica mais difícil de ler. Existem maneiras mais elegantes de conseguir isso: use funções.
precisa
29
-1 Eu vejo o gotoe nem preciso pensar para ver que esse é um código terrível. Uma vez eu tive que manter isso, é muito desagradável. O OP sugere uma alternativa razoável da linguagem C no final da pergunta, e eu a incluí na minha resposta.
Saúde e hth. #: 28414 Alf Alf
56
Não há nada de errado com esse uso limitado e independente do goto. Mas cuidado, o goto é uma droga de passagem e, se você não tomar cuidado, um dia perceberá que ninguém mais come espaguete com os pés, e faz isso há 15 anos, começando depois de pensar: "Eu posso consertar isso caso contrário, um pesadelo com um rótulo ... "
Tim Post
66
Escrevi provavelmente cem mil linhas de código extremamente claro e fácil de manter nesse estilo. As duas coisas principais a entender são (1) disciplina! estabeleça uma diretriz clara para o layout de todas as funções e a viole apenas quando houver necessidade extrema; (2) entenda que o que estamos fazendo aqui é simular exceções em um idioma que não as possui . throwé, de muitas maneiras, pior do que gotoporque, com throwisso, nem está claro no contexto local em que você vai acabar! Use as mesmas considerações de design para o fluxo de controle no estilo goto que usaria para exceções.
Eric Lippert 26/06
131

Esta é uma situação comum e há muitas maneiras comuns de lidar com isso. Aqui está minha tentativa de resposta canônica. Por favor, comente se eu perdi alguma coisa e vou manter esta postagem atualizada.

Esta é uma flecha

O que você está discutindo é conhecido como anti-padrão de seta . É chamada de seta porque a cadeia de ifs aninhados forma blocos de código que se expandem cada vez mais para a direita e depois para a esquerda, formando uma seta visual que "aponta" para o lado direito do painel do editor de código.

Achate a flecha com a guarda

Algumas maneiras comuns de evitar o Arrow são discutidas aqui . O método mais comum é usar um padrão de guarda , no qual o código lida primeiro com os fluxos de exceção e depois lida com o fluxo básico, por exemplo, em vez de

if (ok)
{
    DoSomething();
}
else
{
    _log.Error("oops");
    return;
}

... você usaria ....

if (!ok)
{
    _log.Error("oops");
    return;
} 
DoSomething(); //notice how this is already farther to the left than the example above

Quando houver uma longa série de guardas, isso achatará o código consideravelmente, pois todos os guardas aparecem totalmente à esquerda e seus ifs não estão aninhados. Além disso, você está visualizando o emparelhamento visual da condição lógica com o erro associado, o que torna muito mais fácil saber o que está acontecendo:

Seta:

ok = DoSomething1();
if (ok)
{
    ok = DoSomething2();
    if (ok)
    {
        ok = DoSomething3();
        if (!ok)
        {
            _log.Error("oops");  //Tip of the Arrow
            return;
        }
    }
    else
    {
       _log.Error("oops");
       return;
    }
}
else
{
    _log.Error("oops");
    return;
}

Guarda:

ok = DoSomething1();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething2();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething3();
if (!ok)
{
    _log.Error("oops");
    return;
} 
ok = DoSomething4();
if (!ok)
{
    _log.Error("oops");
    return;
} 

É objetiva e quantificável mais fácil de ler, porque

  1. Os caracteres {e} para um determinado bloco lógico estão mais próximos
  2. A quantidade de contexto mental necessária para entender uma linha específica é menor
  3. A totalidade da lógica associada a uma condição if é mais provável que esteja em uma página
  4. A necessidade de o codificador rolar a página / trilha ocular diminui bastante

Como adicionar código comum no final

O problema com o padrão de guarda é que ele se baseia no que é chamado de "retorno oportunista" ou "saída oportunista". Em outras palavras, ele quebra o padrão de que toda e qualquer função deve ter exatamente um ponto de saída. Este é um problema por dois motivos:

  1. Isso atrapalha algumas pessoas da maneira errada, por exemplo, pessoas que aprenderam a codificar em Pascal aprenderam que uma função = um ponto de saída.
  2. Ele não fornece uma seção de código que é executada na saída, independentemente do assunto , que é o assunto em questão.

Abaixo, forneci algumas opções para contornar essa limitação, usando os recursos de idioma ou evitando o problema completamente.

Opção 1. Você não pode fazer isso: use finally

Infelizmente, como desenvolvedor de c ++, você não pode fazer isso. Mas esta é a resposta número um para idiomas que contêm finalmente uma palavra-chave, pois é exatamente para isso que serve.

try
{
    if (!ok)
    {
        _log.Error("oops");
        return;
    } 
    DoSomething(); //notice how this is already farther to the left than the example above
}
finally
{
    DoSomethingNoMatterWhat();
}

Opção 2. Evite o problema: reestruture suas funções

Você pode evitar o problema dividindo o código em duas funções. Esta solução tem o benefício de trabalhar para qualquer idioma e, além disso, pode reduzir a complexidade ciclomática , que é uma maneira comprovada de reduzir a taxa de defeitos e melhora a especificidade de qualquer teste de unidade automatizado.

Aqui está um exemplo:

void OuterFunction()
{
    DoSomethingIfPossible();
    DoSomethingNoMatterWhat();
}

void DoSomethingIfPossible()
{
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    DoSomething();
}

Opção 3. Truque de idiomas: use um loop falso

Outro truque comum que vejo é usar while (true) e break, como mostrado nas outras respostas.

while(true)
{
     if (!ok) break;
     DoSomething();
     break;  //important
}
DoSomethingNoMatterWhat();

Embora isso seja menos "honesto" do que usado goto, é menos propenso a ser confuso ao refatorar, pois marca claramente os limites do escopo da lógica. Um codificador ingênuo que corta e cola seus rótulos ou suas gotodeclarações pode causar grandes problemas! (E, francamente, o padrão é tão comum agora, acho que comunica claramente a intenção e, portanto, não é "desonesto").

Existem outras variantes dessas opções. Por exemplo, um poderia usar em switchvez de while. Qualquer construção de idioma com uma breakpalavra - chave provavelmente funcionaria.

Opção 4. Aproveite o ciclo de vida do objeto

Uma outra abordagem aproveita o ciclo de vida do objeto. Use um objeto de contexto para carregar seus parâmetros (algo que nosso exemplo ingênuo desconfia) e descartá-lo quando terminar.

class MyContext
{
   ~MyContext()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    MyContext myContext;
    ok = DoSomething(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingElse(myContext);
    if (!ok)
    {
        _log.Error("Oops");
        return;
    }
    ok = DoSomethingMore(myContext);
    if (!ok)
    {
        _log.Error("Oops");
    }

    //DoSomethingNoMatterWhat will be called when myContext goes out of scope
}

Nota: Certifique-se de entender o ciclo de vida do objeto de seu idioma preferido. Você precisa de algum tipo de coleta de lixo determinística para que isso funcione, ou seja, você precisa saber quando o destruidor será chamado. Em alguns idiomas, você precisará usar em Disposevez de um destruidor.

Opção 4.1. Aproveite o ciclo de vida do objeto (padrão de wrapper)

Se você usar uma abordagem orientada a objetos, pode fazê-lo corretamente. Esta opção usa uma classe para "agrupar" os recursos que requerem limpeza, bem como suas outras operações.

class MyWrapper 
{
   bool DoSomething() {...};
   bool DoSomethingElse() {...}


   void ~MyWapper()
   {
        DoSomethingNoMatterWhat();
   }
}

void MainMethod()
{
    bool ok = myWrapper.DoSomething();
    if (!ok)
        _log.Error("Oops");
        return;
    }
    ok = myWrapper.DoSomethingElse();
    if (!ok)
       _log.Error("Oops");
        return;
    }
}
//DoSomethingNoMatterWhat will be called when myWrapper is destroyed

Novamente, certifique-se de entender o seu ciclo de vida do objeto.

Opção 5. Truque de idiomas: use avaliação de curto-circuito

Outra técnica é tirar proveito da avaliação de curto-circuito .

if (DoSomething1() && DoSomething2() && DoSomething3())
{
    DoSomething4();
}
DoSomethingNoMatterWhat();

Esta solução tira proveito da maneira como o operador && funciona. Quando o lado esquerdo de && é avaliado como falso, o lado direito nunca é avaliado.

Esse truque é mais útil quando o código compacto é necessário e quando o código provavelmente não recebe muita manutenção, por exemplo, você está implementando um algoritmo conhecido. Para uma codificação mais geral, a estrutura desse código é muito frágil; mesmo uma pequena alteração na lógica pode desencadear uma reescrita total.

John Wu
fonte
12
Finalmente? C ++ não possui uma cláusula Finalmente. Um objeto com um destruidor é usado em situações em que você acha que precisa de uma cláusula Finalmente.
Bill Porta
1
Editado para acomodar os dois comentários acima.
John Wu
2
Com código trivial (por exemplo, no meu exemplo), os padrões aninhados são provavelmente mais compreensíveis. Com o código do mundo real (que pode abranger várias páginas), o padrão de proteção é objetivamente mais fácil de ler, pois exigirá menos rolagem e menos rastreamento visual, por exemplo, a distância média de {a} é menor.
John Wu
1
Vi um padrão aninhado em que o código não estava mais visível em uma tela 1920 x 1080 ... Tente descobrir qual código de tratamento de erros será executado se a terceira ação falhar ... Eu usei o {...} (0) em vez de modo que você não precisa a ruptura final (por outro lado, while (true) {...} permite "continuar" para começar tudo de novo.
gnasher729
2
Sua opção 4 é realmente um vazamento de memória no C ++ (ignorando o pequeno erro de sintaxe). Não se usa "new" nesse tipo de caso, basta dizer "MyContext myContext;".
Sumudu Fernando
60

Apenas faça

if( executeStepA() && executeStepB() && executeStepC() )
{
    // ...
}
executeThisFunctionInAnyCase();

É simples assim.


Devido a três edições em que cada uma mudou fundamentalmente a pergunta (quatro se uma conta a revisão de volta à versão 1), incluo o exemplo de código ao qual estou respondendo:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}

executeThisFunctionInAnyCase();
Felicidades e hth. - Alf
fonte
14
Eu respondi isso (de forma mais concisa) em resposta à primeira versão da pergunta, e ela recebeu 20 votos positivos antes de ser excluída por Bill the Lizard, depois de alguns comentários e edição por Darkness Races in Orbit.
Saúde e hth. - Alf
1
@ Cheersandhth-Alf: Não acredito que foi modificado. Isso é muito ruim. (+1)
O Croissant Paramagnético
2
Meu +1 à sua coragem! (3 vezes importa: D).
haccks
É completamente importante que os programadores iniciantes aprendam coisas como a execução de ordens com vários "ands" booleanos, como isso é diferente em diferentes idiomas etc. E essa resposta é um ótimo indicador disso. Mas é apenas um "não iniciador" na programação comercial normal. Não é simples, não é sustentável, não é modificável. Claro, se você está apenas escrevendo um "código de cowboy" rápido, faça isso. Mas ele simplesmente não tem nenhuma conexão com a engenharia de software cotidiana, como praticada hoje. BTW pena do ridículo "edit-confusão" que você teve de suportar aqui, @cheers :)
Fattie
1
@ JoeBlow: Estou com Alf nisso - acho a &&lista muito mais clara. Geralmente eu quebro as condições para separar linhas e adiciono um final // explanationa cada .... No final, é muito menos código para se olhar, e depois que você entende como &&funciona, não há esforço mental em andamento. Minha impressão é que a maioria dos programadores profissionais de C ++ estaria familiarizada com isso, mas como você diz em diferentes setores / projetos, o foco e a experiência são diferentes.
Tony Delroy
35

Na verdade, existe uma maneira de adiar ações no C ++: fazendo uso do destruidor de um objeto.

Supondo que você tenha acesso ao C ++ 11:

class Defer {
public:
    Defer(std::function<void()> f): f_(std::move(f)) {}
    ~Defer() { if (f_) { f_(); } }

    void cancel() { f_ = std::function<void()>(); }

private:
    Defer(Defer const&) = delete;
    Defer& operator=(Defer const&) = delete;

    std::function<void()> f_;
}; // class Defer

E então usando esse utilitário:

int foo() {
    Defer const defer{&executeThisFunctionInAnyCase}; // or a lambda

    // ...

    if (!executeA()) { return 1; }

    // ...

    if (!executeB()) { return 2; }

    // ...

    if (!executeC()) { return 3; }

    // ...

    return 4;
} // foo
Matthieu M.
fonte
67
Isso é apenas uma ofuscação completa para mim. Realmente não entendo por que tantos programadores de C ++ gostam de resolver um problema, lançando o maior número possível de recursos de linguagem até o ponto em que todos se esqueceram do problema que você estava realmente resolvendo: eles não se importam mais, porque são agora tão interessado em usar todos esses recursos de linguagem exótica. E a partir daí, você pode se manter ocupado por dias e semanas escrevendo o meta-código, mantendo o meta-código e escrevendo-o para manipular o meta-código.
Lundin
24
@Lundin: Bem, eu não entendo como alguém pode ficar satisfeito com o código quebradiço que será quebrado assim que um início / interrupção / retorno antecipado for introduzido ou uma exceção for lançada. Por outro lado, essa solução é resiliente em caso de manutenção futura e depende apenas do fato de que os destruidores são executados durante o desenrolamento, o que é um dos recursos mais importantes do C ++. Qualificar isso de exótico, embora seja o princípio subjacente que alimenta todos os contêineres Padrão, é divertido, para dizer o mínimo.
Matthieu M.
7
@Lundin: O benefício do código de Matthieu é que ele executeThisFunctionInAnyCase();será executado mesmo se foo();gerar uma exceção. Ao escrever código com exceção de segurança, é uma boa prática colocar todas essas funções de limpeza em um destruidor.
Brian
6
@ Brian Então não lance nenhuma exceção foo(). E se você fizer isso, pegue. Problema resolvido. Corrija os erros, corrigindo-os, não escrevendo soluções alternativas.
Lundin
18
@Lundin: a Deferclasse é um pequeno pedaço de código reutilizável que permite fazer qualquer limpeza de final de bloco, de uma maneira segura e excepcional. é mais conhecido como um protetor de escopo . sim, qualquer uso de uma proteção de escopo pode ser expresso de outras maneiras mais manuais, assim como qualquer forloop pode ser expresso como um bloco e um whileloop, que por sua vez podem ser expressos com ife goto, que podem ser expressos em linguagem assembly, se você desejar, ou para aqueles que são verdadeiros mestres, alterando os bits da memória através de raios cósmicos dirigidos pelo efeito borboleta de grunhidos e cantos curtos especiais. mas por que fazer isso.
Saúde e hth. -
34

Existe uma boa técnica que não precisa de uma função adicional do wrapper com as instruções de retorno (o método prescrito pelo Itjax). Utiliza um while(0)pseudo-loop do. Os while (0)garante que ele não é realmente um laço, mas executados apenas uma vez. No entanto, a sintaxe do loop permite o uso da instrução break.

void foo()
{
  // ...
  do {
      if (!executeStepA())
          break;
      if (!executeStepB())
          break;
      if (!executeStepC())
          break;
  }
  while (0);
  // ...
}
Debasis
fonte
11
Usar uma função com retornos múltiplos é semelhante, mas mais legível, na minha opinião.
precisa
4
Sim, definitivamente é mais legível ... no entanto, do ponto de vista da eficiência, a sobrecarga adicional de chamada de função (passagem e retorno de parâmetro) pode ser evitada com a construção do {} while (0).
Debasis 27/06
5
Você ainda está livre para fazer a função inline. De qualquer forma, é uma boa técnica para saber, pois ajuda mais do que esse problema.
Ruslan
2
@Lundin Você precisa levar em consideração a localidade do código, espalhar o código por muitos lugares também tem problemas.
API-Beast
3
Na minha experiência, esse é um idioma muito incomum. Levaria um tempo para descobrir o que diabos estava acontecendo, e isso é um mau sinal quando estou revisando o código. Dado que parece não ter vantagens sobre as outras abordagens mais comuns e, portanto, mais legíveis, não pude assinar.
Cody Gray
19

Você também pode fazer isso:

bool isOk = true;
std::vector<bool (*)(void)> funcs; //vector of function ptr

funcs.push_back(&executeStepA);
funcs.push_back(&executeStepB);
funcs.push_back(&executeStepC);
//...

//this will stop at the first false return
for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

Dessa forma, você tem um tamanho de crescimento linear mínimo, uma linha por chamada e é facilmente sustentável.


EDIT : (Obrigado @Unda) Não é um grande fã porque você perde visibilidade IMO:

bool isOk = true;
auto funcs { //using c++11 initializer_list
    &executeStepA,
    &executeStepB,
    &executeStepC
};

for (auto it = funcs.begin(); it != funcs.end() && isOk; ++it) 
    isOk = (*it)();
if (isOk)
 //doSomeStuff
executeThisFunctionInAnyCase();

fonte
1
Era sobre as chamadas de função acidentais dentro push_back (), mas você fixa-lo de qualquer maneira :)
Quentin
4
Eu adoraria saber por que isso teve um voto negativo. Supondo que as etapas de execução sejam simétricas, como parecem ser, são limpas e sustentáveis.
ClickRick 26/06
1
Embora isso possa parecer mais limpo. Pode ser mais difícil de entender para as pessoas e definitivamente é mais difícil de entender para o compilador!
Roy T.
1
Tratar suas funções mais como dados geralmente é uma boa idéia - depois de fazer isso, você também notará refatorações subseqüentes. Ainda melhor é que cada peça que você está manipulando é uma referência de objeto, não apenas uma referência de função - isso lhe dará ainda mais capacidade de melhorar seu código na linha.
Bill K
3
Pouco projetada demais para um caso trivial, mas essa técnica definitivamente possui um recurso interessante que outras não: Você pode alterar a ordem de execução e o [número de] funções chamadas em tempo de execução, e isso é bom :)
Xocoatzin
18

Isso funcionaria? Eu acho que isso é equivalente ao seu código.

bool condition = true; // using only one boolean variable
if (condition) condition = executeStepA();
if (condition) condition = executeStepB();
if (condition) condition = executeStepC();
...
executeThisFunctionInAnyCase();
sampathsris
fonte
3
Eu costumo chamar a variável okao usar a mesma variável assim.
27614 Macke
1
Eu ficaria muito interessado em saber por que os votos negativos. O que está acontecendo de errado aqui?
ABCplus 8/07
1
compare sua resposta com a abordagem de curto-circuito para complexidade ciclomática.
AlphaGoku 23/02
14

Supondo que o código desejado seja o que eu vejo atualmente:

bool conditionA = executeStepA();
if (conditionA){
    bool conditionB = executeStepB();
    if (conditionB){
        bool conditionC = executeStepC();
        if (conditionC){
            ...
        }
    }
}    
executeThisFunctionInAnyCase();

Eu diria que a abordagem correta, na medida em que é mais simples de ler e mais fácil de manter, teria menos níveis de indentação, que é (atualmente) o objetivo declarado da questão.

// Pre-declare the variables for the conditions
bool conditionA = false;
bool conditionB = false;
bool conditionC = false;

// Execute each step only if the pre-conditions are met
conditionA = executeStepA();
if (conditionA)
    conditionB = executeStepB();
if (conditionB)
    conditionC = executeStepC();
if (conditionC) {
    ...
}

// Unconditionally execute the 'cleanup' part.
executeThisFunctionInAnyCase();

Isso evita qualquer necessidade de gotos, exceções, whileloops fictícios ou outras construções difíceis e simplesmente continua o trabalho simples em mãos.

ClickRick
fonte
Ao usar loops, geralmente é aceitável usar returne breakpular para fora do loop sem a necessidade de introduzir variáveis ​​extras "flag". Nesse caso, o uso de um goto seria igualmente inocente - lembre-se de que você está trocando uma complexidade extra de goto por uma complexidade variável mutável extra.
hugomg
2
@ hugomg As variáveis ​​estavam na pergunta original. Não há complexidade extra aqui. Foram feitas suposições sobre a questão (por exemplo, que as variáveis ​​são necessárias no código redigido) para que elas fossem retidas. Se não forem necessários, o código poderá ser simplificado, mas, dada a natureza incompleta da questão, não há outra suposição que possa ser feita validamente.
ClickRick 26/06
Abordagem muito útil, esp. para uso por profissionais autônomos newbie, fornece uma solução mais limpa, sem inconvenientes. Observo que também não depende de stepster a mesma assinatura ou mesmo de ser funções, em vez de blocos. Eu posso ver isso sendo usado como um refator de primeira passagem, mesmo onde uma abordagem mais sofisticada é válida.
Keith
12

A declaração de quebra poderia ser usada de alguma maneira?

Talvez não seja a melhor solução, mas você pode colocar suas instruções em um do .. while (0)loop e usar breakinstruções em vez de return.

ouah
fonte
2
Não fui eu quem diminuiu o voto, mas isso seria um abuso de uma construção em loop para algo em que o efeito é o que é atualmente desejado, mas inevitavelmente resultaria em dor. Provavelmente para o próximo desenvolvedor que precisar mantê-lo em dois anos após a mudança para outro projeto.
ClickRick
3
O @ClickRick usando do .. while (0)para definições de macro também está abusando de loops, mas é considerado OK.
OuJun
1
Talvez, mas existem maneiras mais limpas de alcançá-lo.
ClickRick 26/06
4
@ClickRick o único objetivo da minha resposta é responder A declaração de quebra pode ser usada de alguma maneira e a resposta é sim, as primeiras palavras da minha resposta sugerem que essa pode não ser a solução a ser usada.
ouah
2
Esta resposta deve ser apenas um comentário
msmucker0527
12

Você pode colocar todas as ifcondições, formatadas como você deseja, em uma função própria, para o retorno executar a executeThisFunctionInAnyCase()função.

No exemplo base do OP, o teste e a execução da condição podem ser divididos como tal;

void InitialSteps()
{
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
}

E então chamado como tal;

InitialSteps();
executeThisFunctionInAnyCase();

Se lambdas C ++ 11 estiverem disponíveis (não havia tag C ++ 11 no OP, mas elas ainda podem ser uma opção), podemos renunciar à função separada e agrupá-la em uma lambda.

// Capture by reference (variable access may be required)
auto initialSteps = [&]() {
  // any additional code
  bool conditionA = executeStepA();
  if (!conditionA)
    return;
  // any additional code
  bool conditionB = executeStepB();
  if (!conditionB)
    return;
  // any additional code
  bool conditionC = executeStepC();
  if (!conditionC)
    return;
};

initialSteps();
executeThisFunctionInAnyCase();
Niall
fonte
10

Se você não gosta gotoe não gosta de do { } while (0);loops e gosta de usar C ++, também pode usar um lambda temporário para ter o mesmo efeito.

[&]() { // create a capture all lambda
  if (!executeStepA()) { return; }
  if (!executeStepB()) { return; }
  if (!executeStepC()) { return; }
}(); // and immediately call it

executeThisFunctionInAnyCase();
Alex
fonte
1
if você não gosta Goto && você não gosta do {} while (0) && Você gosta C ++ ... Desculpe, não pude resistir, mas essa última condição falha porque a questão é marcado c , bem como c ++
ClickRick
@ClickRick é sempre difícil agradar a todos. Na minha opinião, não existe C / C ++ que você normalmente codifica em um deles e o uso do outro é desaprovado.
Alex
9

As cadeias de IF / ELSE no seu código não são o problema de idioma, mas o design do seu programa. Se você é capaz de re-fatorar ou reescrever seu programa, gostaria de sugerir que você procure em Design Patterns ( http://sourcemaking.com/design_patterns ) para encontrar uma solução melhor.

Normalmente, quando você vê muitos FIs e outros itens no seu código, é uma oportunidade de implementar o Padrão de Design da Estratégia ( http://sourcemaking.com/design_patterns/strategy/c-sharp-dot-net ) ou talvez uma combinação de outros padrões.

Tenho certeza de que existem alternativas para escrever uma longa lista de if / else, mas duvido que elas mudem tudo, exceto que a cadeia ficará bonita para você (no entanto, a beleza está nos olhos de quem vê ainda se aplica ao código também:-) ) . Você deve se preocupar com coisas como (em 6 meses quando eu tiver uma nova condição e não me lembro de nada sobre esse código, poderei adicioná-lo facilmente? Ou, se a cadeia mudar, com que rapidez e sem erros serei implementado)

Roman Mik
fonte
9

Você acabou de fazer isso ..

coverConditions();
executeThisFunctionInAnyCase();

function coverConditions()
 {
 bool conditionA = executeStepA();
 if (!conditionA) return;
 bool conditionB = executeStepB();
 if (!conditionB) return;
 bool conditionC = executeStepC();
 if (!conditionC) return;
 }

99 vezes de 100, esta é a única maneira de fazê-lo.

Nunca, jamais, tente fazer algo "complicado" no código do computador.


By the way, eu tenho certeza que o seguinte é a solução real que você tinha em mente ...

A instrução continue é crítica na programação algorítmica. (Tanto quanto, a instrução goto é crítica na programação algorítmica.)

Em muitas linguagens de programação, você pode fazer isso:

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    {
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }
    
    NSLog(@"code g");
    }

(Observe antes de tudo: blocos simples como esse exemplo são uma parte crítica e importante da escrita de códigos bonitos, principalmente se você estiver lidando com programação "algorítmica".)

Novamente, é exatamente isso que você tinha em sua cabeça, certo? E essa é a maneira bonita de escrever, para que você tenha bons instintos.

No entanto, tragicamente, na versão atual do objetivo-c (além disso - não conheço o Swift, desculpe), há um recurso que pode ser subido onde ele verifica se o bloco envolvente é um loop.

insira a descrição da imagem aqui

Aqui está como você contorna isso ...

-(void)_testKode
    {
    NSLog(@"code a");
    NSLog(@"code b");
    NSLog(@"code c\n");
    
    int x = 69;
    
    do{
    
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    
    }while(false);
    
    NSLog(@"code g");
    }

Então não esqueça disso ..

faça {} enquanto (falso);

significa apenas "faça este bloco uma vez".

ou seja, não há absolutamente nenhuma diferença entre escrever do{}while(false);e simplesmente escrever {}.

Isso agora funciona perfeitamente como você queria ... aqui está a saída ...

insira a descrição da imagem aqui

Então, é possível que seja assim que você vê o algoritmo em sua cabeça. Você deve sempre tentar escrever o que está na sua cabeça. (Particularmente se você não estiver sóbrio, porque é aí que a bonita sai! :))

Em projetos "algorítmicos", onde isso acontece muito, no objetivo-c, sempre temos uma macro como ...

#define RUNONCE while(false)

... então você pode fazer isso ...

-(void)_testKode
    {
    NSLog(@"code a");
    int x = 69;
    
    do{
    if ( x == 13 )
        {
        NSLog(@"code d---\n");
        continue;
        }
    if ( x == 69 )
        {
        NSLog(@"code e---\n");
        continue;
        }
    if ( x == 13 )
        {
        NSLog(@"code f---\n");
        continue;
        }
    }RUNONCE
    
    NSLog(@"code g");
    }

Existem dois pontos:

a, mesmo que seja estúpido que o objetivo-c verifique o tipo de bloco em que uma instrução continue está, é preocupante "lutar contra isso". Portanto, é uma decisão difícil.

b, existe a pergunta que você deve recuar, no exemplo, nesse bloco? Eu perco o sono com perguntas assim, então não posso aconselhar.

Espero que ajude.

Fattie
fonte
Você perdeu a segunda resposta melhor votada .
Palec
Você colocou a recompensa para obter mais rep? :)
TMS
Em vez de colocar todos esses comentários no if, você também pode usar nomes de função mais descritivos e colocar os comentários nas funções.
Thomas Ahle
Que nojo. Vou usar a solução de 1 linha que lê concisamente com a avaliação de curto-circuito (que está em idiomas há mais de 20 anos e é bem conhecida) nessa bagunça todos os dias. Acho que podemos concordar que estamos felizes por não trabalhar um com o outro.
M2tM
8

Faça com que suas funções de execução gerem uma exceção se falharem em vez de retornar falso. Em seguida, seu código de chamada pode ficar assim:

try {
    executeStepA();
    executeStepB();
    executeStepC();
}
catch (...)

É claro que estou assumindo que, no seu exemplo original, a etapa de execução retornaria falsa apenas no caso de um erro ocorrer dentro da etapa?

Rik
fonte
3
usando exceção ao fluxo de controle é muitas vezes consideram como má prática e código mal cheiroso
user902383
8

Já existem muitas boas respostas, mas a maioria delas parece compensar parte (reconhecidamente muito pouco) da flexibilidade. Uma abordagem comum que não exige essa troca é a adição de uma variável de status / manutenção . O preço é, obviamente, um valor extra para acompanhar:

bool ok = true;
bool conditionA = executeStepA();
// ... possibly edit conditionA, or just ok &= executeStepA();
ok &= conditionA;

if (ok) {
    bool conditionB = executeStepB();
    // ... possibly do more stuff
    ok &= conditionB;
}
if (ok) {
    bool conditionC = executeStepC();
    ok &= conditionC;
}
if (ok && additionalCondition) {
    // ...
}

executeThisFunctionInAnyCase();
// can now also:
return ok;
blgt
fonte
Por que ok &= conditionX;e não simplesmente ok = conditionX;?
ABCplus 27/06/14
@ user3253359 Em muitos casos, sim, você pode fazer isso. Esta é uma demonstração conceitual; no código de trabalho
tentaríamos
+1 Uma das poucas respostas limpas e sustentáveis ​​que funcionam em c , conforme estipulado na pergunta.
ClickRick
6

No C ++ (a pergunta está marcada como C e C ++), se você não pode alterar as funções para usar exceções, ainda pode usar o mecanismo de exceção se escrever uma pequena função auxiliar como

struct function_failed {};
void attempt(bool retval)
{
  if (!retval)
    throw function_failed(); // or a more specific exception class
}

Em seguida, seu código pode ler da seguinte maneira:

try
{
  attempt(executeStepA());
  attempt(executeStepB());
  attempt(executeStepC());
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();

Se você gosta de sintaxe sofisticada, pode fazê-lo funcionar por meio de conversão explícita:

struct function_failed {};
struct attempt
{
  attempt(bool retval)
  {
    if (!retval)
      throw function_failed();
  }
};

Então você pode escrever seu código como

try
{
  (attempt) executeStepA();
  (attempt) executeStepB();
  (attempt) executeStepC();
}
catch (function_failed)
{
  // -- this block intentionally left empty --
}

executeThisFunctionInAnyCase();
celtschk
fonte
A refatoração de verificações de valor em exceções não é necessariamente um bom caminho a percorrer, há consideráveis ​​exceções de desenrolar de sobrecarga.
John Wu
4
-1 Usar exceções para fluxo normal em C ++ como este é uma prática ruim de programação. Em C ++, as exceções devem ser reservadas para circunstâncias excepcionais.
Jack Aidley
1
No texto da pergunta (ênfase minha): "As funções executeStepX devem ser executadas se e somente se as anteriores tiverem êxito. " Em outras palavras, o valor de retorno é usado para indicar falha. Ou seja, trata-se de tratamento de erros (e seria de esperar que as falhas sejam excepcionais). O tratamento de erros é exatamente para o qual foram criadas as exceções.
Celtschk
1
Não. Primeiro, foram criadas exceções para permitir a propagação de erros , não a manipulação de erros ; segundo, "As funções executeStepX devem ser executadas se e somente se as anteriores tiverem êxito." não significa que false booleano retornado pela função anterior indique um caso obviamente excepcional / incorreto. Sua declaração é, portanto, não sequitur . O tratamento de erros e a higienização do fluxo podem ser implementados de vários modos diferentes; as exceções são uma ferramenta que permite a propagação de erros e o tratamento de erros fora do local , além de serem excelentes.
6

Se seu código é tão simples quanto o seu exemplo e seu idioma suporta avaliações de curto-circuito, você pode tentar o seguinte:

StepA() && StepB() && StepC() && StepD();
DoAlways();

Se você estiver passando argumentos para suas funções e recuperando outros resultados para que seu código não possa ser gravado da maneira anterior, muitas das outras respostas seriam mais adequadas ao problema.

Noctis Skytower
fonte
De fato, editei minha pergunta para explicar melhor o tópico, mas ela foi rejeitada por não invalidar a maioria das respostas. : \
ABCplus 01/07/2014
Eu sou um novo usuário no SO e um programador iniciante. Duas perguntas, então: existe o risco de que outra pergunta como a que você disse seja marcada como duplicada por causa desta pergunta? Outro ponto é: como um usuário / programador novato em SO pode escolher a melhor resposta entre todos os que existem (quase bom, suponho ..)?
ABCplus
6

Para C ++ 11 e além, uma boa abordagem pode ser a implementação de um sistema de saída de escopo semelhante ao mecanismo de escopo (saída) de D.

Uma maneira possível de implementá-lo é usar C ++ 11 lambdas e algumas macros auxiliares:

template<typename F> struct ScopeExit 
{
    ScopeExit(F f) : fn(f) { }
    ~ScopeExit() 
    { 
         fn();
    }

    F fn;
};

template<typename F> ScopeExit<F> MakeScopeExit(F f) { return ScopeExit<F>(f); };

#define STR_APPEND2_HELPER(x, y) x##y
#define STR_APPEND2(x, y) STR_APPEND2_HELPER(x, y)

#define SCOPE_EXIT(code)\
    auto STR_APPEND2(scope_exit_, __LINE__) = MakeScopeExit([&](){ code })

Isso permitirá que você retorne mais cedo da função e garanta que qualquer código de limpeza definido seja sempre executado na saída do escopo:

SCOPE_EXIT(
    delete pointerA;
    delete pointerB;
    close(fileC); );

if (!executeStepA())
    return;

if (!executeStepB())
    return;

if (!executeStepC())
    return;

As macros são realmente apenas decoração. MakeScopeExit()pode ser usado diretamente.

glampert
fonte
Não há necessidade de macros para fazer isso funcionar. E [=]geralmente está errado para um lambda com escopo definido.
Yakk - Adam Nevraumont
Sim, as macros são apenas para decoração e podem ser jogadas fora. Mas você não diria que a captura por valor é a abordagem "genérica" ​​mais segura?
glampert
1
não: se o seu lambda não persistir além do escopo atual em que o lambda é criado, use [&]: é seguro e minimamente surpreendente. Captura por valor somente quando o lambda (ou cópias) poderia sobreviver mais tempo do que o escopo no momento da declaração ...
Yakk - Adam Nevraumont
Sim, isso faz sentido. Eu vou mudar isso. Obrigado!
glampert
6

Por que ninguém está dando a solução mais simples? : D

Se todas as suas funções tiverem a mesma assinatura, você poderá fazê-lo desta maneira (no idioma C):

bool (*step[])() = {
    &executeStepA,
    &executeStepB,
    &executeStepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    bool condition = step[i]();

    if (!condition) {
        break;
    }
}

executeThisFunctionInAnyCase();

Para uma solução C ++ limpa, você deve criar uma classe de interface que contenha um método execute e agrupe suas etapas em objetos.
Em seguida, a solução acima terá a seguinte aparência:

Step *steps[] = {
    stepA,
    stepB,
    stepC,
    ... 
};

for (int i = 0; i < numberOfSteps; i++) {
    Step *step = steps[i];

    if (!step->execute()) {
        break;
    }
}

executeThisFunctionInAnyCase();
Gaskoin
fonte
5

Supondo que você não precise de variáveis ​​de condição individuais, inverter os testes e usar o else-falthrough como o caminho "ok" permitiria obter um conjunto mais vertical de instruções if / else:

bool failed = false;

// keep going if we don't fail
if (failed = !executeStepA())      {}
else if (failed = !executeStepB()) {}
else if (failed = !executeStepC()) {}
else if (failed = !executeStepD()) {}

runThisFunctionInAnyCase();

Omitir a variável com falha torna o código um pouco IMO obscuro.

Declarar as variáveis ​​internas é bom, não se preocupe com = vs ==.

// keep going if we don't fail
if (bool failA = !executeStepA())      {}
else if (bool failB = !executeStepB()) {}
else if (bool failC = !executeStepC()) {}
else if (bool failD = !executeStepD()) {}
else {
     // success !
}

runThisFunctionInAnyCase();

Isso é obscuro, mas compacto:

// keep going if we don't fail
if (!executeStepA())      {}
else if (!executeStepB()) {}
else if (!executeStepC()) {}
else if (!executeStepD()) {}
else { /* success */ }

runThisFunctionInAnyCase();
Macke
fonte
5

Parece uma máquina de estado, o que é útil porque você pode implementá-la facilmente com um padrão de estado .

Em Java, seria algo parecido com isto:

interface StepState{
public StepState performStep();
}

Uma implementação funcionaria da seguinte maneira:

class StepA implements StepState{ 
    public StepState performStep()
     {
         performAction();
         if(condition) return new StepB()
         else return null;
     }
}

E assim por diante. Em seguida, você pode substituir a condição big if por:

Step toDo = new StepA();
while(toDo != null)
      toDo = toDo.performStep();
executeThisFunctionInAnyCase();
CarrKnight
fonte
5

Como Rommik mencionou, você pode aplicar um padrão de design para isso, mas eu usaria o padrão Decorator em vez de Strategy, pois você deseja encadear chamadas. Se o código for simples, eu usaria uma das respostas bem estruturadas para evitar o aninhamento. No entanto, se for complexo ou exigir encadeamento dinâmico, o padrão Decorator é uma boa opção. Aqui está um diagrama de classes yUML :

diagrama de classes yUML

Aqui está um exemplo de programa LinqPad C #:

void Main()
{
    IOperation step = new StepC();
    step = new StepB(step);
    step = new StepA(step);
    step.Next();
}

public interface IOperation 
{
    bool Next();
}

public class StepA : IOperation
{
    private IOperation _chain;
    public StepA(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step A success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepB : IOperation
{
    private IOperation _chain;
    public StepB(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {   
        bool localResult = false;

        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to false, 
            // to show breaking out of the chain
        localResult = false;
        Console.WriteLine("Step B success={0}", localResult);

        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

public class StepC : IOperation
{
    private IOperation _chain;
    public StepC(IOperation chain=null)
    {
        _chain = chain;
    }

    public bool Next() 
    {
        bool localResult = false;
        //do work
        //...
        // set localResult to success of this work
        // just for this example, hard coding to true
        localResult = true;
        Console.WriteLine("Step C success={0}", localResult);
        //then call next in chain and return
        return (localResult && _chain != null) 
            ? _chain.Next() 
            : true;
    }
}

O melhor livro para ler sobre padrões de design, IMHO, é Head First Design Patterns .

O que seria legal
fonte
Qual é o benefício disso em relação a algo como a resposta de Jefffrey?
Dason 02/07/19
muito mais resistente à mudança, quando os requisitos mudam, essa abordagem é mais simples de gerenciar, sem muito conhecimento de domínio. Especialmente quando você considera a profundidade e a duração de algumas seções de ifs aninhados. Tudo isso pode ficar muito frágil e, portanto, com alto risco de se trabalhar. Não me entenda mal, alguns cenários de otimização podem resultar em você copiar isso e voltar ao ifs, mas em 99% das vezes isso é bom. Mas o ponto é que, quando você chega a esse nível, não se importa com a manutenção, precisa do desempenho.
John Nicholas
4

Várias respostas sugeriram um padrão que eu vi e usei muitas vezes, especialmente em programação de rede. Nas pilhas de rede, geralmente há uma longa sequência de solicitações, qualquer uma das quais pode falhar e interromper o processo.

O padrão comum era usar do { } while (false);

Eu usei uma macro para while(false)fazê-lo do { } once;O padrão comum era:

do
{
    bool conditionA = executeStepA();
    if (! conditionA) break;
    bool conditionB = executeStepB();
    if (! conditionB) break;
    // etc.
} while (false);

Esse padrão era relativamente fácil de ler e permitia o uso de objetos que destruíam adequadamente e também evitavam vários retornos, facilitando um pouco a etapa e a depuração.

Bill Door
fonte
4

Para melhorar a resposta C ++ 11 de Mathieu e evitar o custo de tempo de execução incorrido com o uso de std::function, sugiro usar o seguinte

template<typename functor>
class deferred final
{
public:
    template<typename functor2>
    explicit deferred(functor2&& f) : f(std::forward<functor2>(f)) {}
    ~deferred() { this->f(); }

private:
    functor f;
};

template<typename functor>
auto defer(functor&& f) -> deferred<typename std::decay<functor>::type>
{
    return deferred<typename std::decay<functor>::type>(std::forward<functor>(f));
}

Essa classe de modelo simples aceita qualquer functor que possa ser chamado sem nenhum parâmetro e o faz sem alocações dinâmicas de memória e, portanto, está melhor em conformidade com o objetivo de abstração do C ++ sem sobrecarga desnecessária. O modelo de função adicional existe para simplificar o uso pela dedução de parâmetro do modelo (que não está disponível para os parâmetros do modelo de classe)

Exemplo de uso:

auto guard = defer(executeThisFunctionInAnyCase);
bool conditionA = executeStepA();
if (!conditionA) return;
bool conditionB = executeStepB();
if (!conditionB) return;
bool conditionC = executeStepC();
if (!conditionC) return;

Assim como a resposta de Mathieu, esta solução é totalmente segura contra exceções e executeThisFunctionInAnyCaseserá chamada em todos os casos. Deveria executeThisFunctionInAnyCaselançar, os destruidores são implicitamente marcados noexcepte, portanto, uma chamada para std::terminateseria emitida em vez de causar uma exceção a ser lançada durante o desenrolamento da pilha.

Joe
fonte
+1 Eu estava procurando por esta resposta para não precisar publicá-la. Você deve avançar perfeitamente functorno deferredconstrutor, sem precisar forçar a move.
Yakk - Adam Nevraumont
@Yakk alterou o construtor para um construtor de encaminhamento
Joe
3

Parece que você deseja fazer todas as suas ligações em um único bloco. Como outros propuseram, você deve usar um whileloop e deixar de usar breakou uma nova função com a qual possa sair return(pode ser mais limpo).

Eu pessoalmente banir goto, mesmo para a saída da função. Eles são mais difíceis de detectar ao depurar.

Uma alternativa elegante que deve funcionar para o seu fluxo de trabalho é criar uma matriz de funções e iterar nessa.

const int STEP_ARRAY_COUNT = 3;
bool (*stepsArray[])() = {
   executeStepA, executeStepB, executeStepC
};

for (int i=0; i<STEP_ARRAY_COUNT; ++i) {
    if (!stepsArray[i]()) {
        break;
    }
}

executeThisFunctionInAnyCase();
AxFab
fonte
Felizmente, o depurador os identifica para você. Se você está depurando e não está percorrendo o código de maneira única, está fazendo errado.
Cody Gray
Não entendo o que você quer dizer, nem por que não pude usar o passo único?
AxFab
3

Como você também tem um bloco de [...] código entre execuções, acho que você tem alocação de memória ou inicialização de objeto. Dessa forma, você precisa se preocupar em limpar tudo o que você já inicializou na saída e também limpá-lo se encontrar algum problema e qualquer uma das funções retornar falso.

Nesse caso, o melhor que eu tive na minha experiência (quando trabalhei com o CryptoAPI) foi criar pequenas classes, no construtor você inicializa seus dados, no destruidor você o inicializa. Cada próxima classe de função deve ser filha da classe de função anterior. Se algo der errado - lance uma exceção.

class CondA
{
public:
    CondA() { 
        if (!executeStepA()) 
            throw int(1);
        [Initialize data]
    }
    ~CondA() {        
        [Clean data]
    }
    A* _a;
};

class CondB : public CondA
{
public:
    CondB() { 
        if (!executeStepB()) 
            throw int(2);
        [Initialize data]
    }
    ~CondB() {        
        [Clean data]
    }
    B* _b;
};

class CondC : public CondB
{
public:
    CondC() { 
        if (!executeStepC()) 
            throw int(3);
        [Initialize data]
    }
    ~CondC() {        
        [Clean data]
    }
    C* _c;
};

E então, no seu código, você só precisa chamar:

shared_ptr<CondC> C(nullptr);
try{
    C = make_shared<CondC>();
}
catch(int& e)
{
    //do something
}
if (C != nullptr)
{
   C->a;//work with
   C->b;//work with
   C->c;//work with
}
executeThisFunctionInAnyCase();

Eu acho que é a melhor solução se cada chamada do ConditionX inicializar algo, alocar memória e etc. Melhor garantir que tudo será limpo.

Arkady
fonte
3

uma maneira interessante é trabalhar com exceções.

try
{
    executeStepA();//function throws an exception on error
    ......
}
catch(...)
{
    //some error handling
}
finally
{
    executeThisFunctionInAnyCase();
}

Se você escrever esse código, está indo de alguma forma na direção errada. Não considero "o problema" ter esse código, mas ter uma "arquitetura" tão confusa.

Dica: discuta esses casos com um desenvolvedor experiente no qual você confia ;-)

Karsten
fonte
Penso que esta ideia não pode substituir todas as cadeias de if. De qualquer forma, em muitos casos, essa é uma abordagem muito boa!
WoIIe
3

Outra abordagem - do - whileloop, apesar de ter sido mencionado antes, não havia nenhum exemplo disso que mostrasse como ele se parece:

do
{
    if (!executeStepA()) break;
    if (!executeStepB()) break;
    if (!executeStepC()) break;
    ...

    break; // skip the do-while condition :)
}
while (0);

executeThisFunctionInAnyCase();

(Bem, já existe uma resposta com whileloop, mas o do - whileloop não verifica redundantemente se é verdade (no início), mas no final xD (isso pode ser ignorado).

Zaffy
fonte
Hey Zaffy - esta resposta tem uma explicação maciça da abordagem do {while ((false)). stackoverflow.com/a/24588605/294884 Duas outras respostas também o mencionaram.
Fattie
É uma pergunta fascinante se é mais elegante usar CONTINUE ou BREAK nesta situação!
Fattie
Hey @JoeBlow Eu vi todas as respostas ... só queria mostrar em vez de falar sobre isso :)
Zaffy
Minha primeira resposta aqui eu disse "Ninguém mencionou isso ..." e imediatamente alguém gentilmente apontou que era a resposta top 2 :)
Fattie
@JoeBlow Eh, você está certo. Vou tentar consertar isso. Eu me sinto como ... xD Obrigado de qualquer maneira, da próxima vez eu vou pagar attetion um pouco mais :)
Zaffy