declaração if - avaliação de curto-circuito vs legibilidade


90

Às vezes, uma ifinstrução pode ser um tanto complicada ou longa, portanto, por uma questão de legibilidade, é melhor extrair chamadas complicadas antes de if.

por exemplo:

if (SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall())
{
    // do stuff
}

nisso

bool b1 = SomeComplicatedFunctionCall();
bool b2 = OtherComplicatedFunctionCall();

if (b1 || b2)
{
    //do stuff
}

(desde exemplo não é que ruim, é apenas para ilustração ... imaginar outras chamadas com múltiplos argumentos, etc.)

Mas com esta extração perdi a avaliação de curto-circuito (SCE).

  1. Eu realmente perco o SCE todas as vezes? Existe algum cenário onde o compilador pode "otimizá-lo" e ainda fornecer SCE?
  2. Existem maneiras de manter a legibilidade aprimorada do segundo snippet sem perder o SCE?

20
A prática mostra que a maioria das respostas sobre o desempenho que você verá aqui ou em outros lugares estão, na maioria dos casos, erradas (4 erradas 1 correto). Meu conselho é sempre fazer um perfil e verificar você mesmo, você evitará "otimizações prematuras" e aprenderá coisas novas.
— Marek R de

25
@MarekR não se trata apenas de desempenho, trata-se de possíveis efeitos colaterais em OtherCunctionCall ...
— relaxxx

3
@David ao referir outros sites, muitas vezes é útil apontar que a postagem cruzada é desaprovada
— mosquito

7
Se a legibilidade for sua principal preocupação, não chame funções com efeitos colaterais dentro de um if condicional
— Morgen

3
Eleitores próximos potenciais: leia a pergunta novamente. A parte (1) não é baseada em opinião, enquanto a parte (2) pode facilmente deixar de ser baseada em opinião por meio de uma edição que remove a referência a qualquer suposta "melhor prática", como estou prestes a fazer.
— duplode

Respostas:


119

Uma solução natural seria assim:

bool b1 = SomeCondition();
bool b2 = b1 || SomeOtherCondition();
bool b3 = b2 || SomeThirdCondition();
// any other condition
bool bn = bn_1 || SomeFinalCondition();

if (bn)
{
  // do stuff
}

Tem a vantagem de ser fácil de compreender, ser aplicável a todos os casos e ter comportamento em curto-circuito.


Esta foi minha solução inicial: um bom padrão em chamadas de método e corpos de loop for é o seguinte:

if (!SomeComplicatedFunctionCall())
   return; // or continue

if (!SomeOtherComplicatedFunctionCall())
   return; // or continue

// do stuff

Obtém-se os mesmos benefícios de desempenho agradáveis ​​da avaliação de curto-circuito, mas o código parece mais legível.


4
@relaxxx: Entendi, mas "mais coisas para fazer depois de if" também é um sinal de que sua função ou método é muito grande e deve ser dividido em outros menores. Nem sempre é a melhor maneira, mas muitas vezes é!
— nperson325681

2
isso viola o princípio da lista branca
— JoulinRouge,

13
@JoulinRouge: Interessante, nunca tinha ouvido falar desse princípio. Eu mesmo prefiro essa abordagem de "curto-circuito" pelos benefícios de legibilidade: ela reduz as indentações e elimina a possibilidade de que algo ocorra após o bloco indentado.
— Matthieu M.

2
É mais legível? Nomeie b2corretamente e você obterá someConditionAndSomeotherConditionIsTrue, não muito significativo. Além disso, tenho que manter um monte de variáveis ​​em minha pilha mental durante este exercício (e tbh até parar de trabalhar neste escopo). Eu escolheria a SJuan76solução número 2 de ou simplesmente colocaria tudo em uma função.
— Nathan Cooper

2
Não li todos os comentários, mas após uma pesquisa rápida, não encontrei uma grande vantagem do primeiro trecho de código, ou seja, a depuração. Colocar coisas diretamente na instrução if em vez de atribuí-las a uma variável de antemão e, em seguida, usar a variável torna a depuração mais difícil do que precisa ser. O uso de variáveis ​​também permite agrupar valores semanticamente, o que aumenta a legibilidade.
— rbaleksandar

31

Tenho tendência a decompor as condições em várias linhas, ou seja:

if( SomeComplicatedFunctionCall()
 || OtherComplicatedFunctionCall()
  ) {

Mesmo ao lidar com vários operadores (&&), você só precisa avançar o recuo com cada par de colchetes. O SCE ainda entra em ação - não há necessidade de usar variáveis. Escrever código dessa maneira tornou-o muito mais legível para mim já há anos. Exemplo mais complexo:

if( one()
 ||( two()> 1337
  &&( three()== 'foo'
   || four()
    )
   )
 || five()!= 3.1415
  ) {

28

Se você tiver longas cadeias de condições e o que manter em curto-circuito, poderá usar variáveis ​​temporárias para combinar várias condições. Tomando seu exemplo, seria possível fazer, por exemplo,

bool b = SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall();
if (b && some_other_expression) { ... }

Se você tiver um compilador compatível com C ++ 11, poderá usar expressões lambda para combinar expressões em funções, semelhante ao anterior:

auto e = []()
{
    return SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall();
};

if (e() && some_other_expression) { ... }

21

1) Sim, você não tem mais SCE. Caso contrário, você teria que

bool b1 = SomeComplicatedFunctionCall();
bool b2 = OtherComplicatedFunctionCall();

funciona de uma forma ou de outra, dependendo se houver uma ifdeclaração posterior. Muito complexo.

2) Isso é baseado em opinião, mas para expressões razoavelmente complexas, você pode fazer:

if (SomeComplicatedFunctionCall()
    || OtherComplicatedFunctionCall()) {

Se for muito complexo, a solução óbvia é criar uma função que avalie a expressão e chamá-la.


21

Você também pode usar:

bool b = someComplicatedStuff();
b = b || otherComplicatedStuff(); // it has to be: b = b || ...;  b |= ...; is bitwise OR and SCE is not working then 

e o SCE funcionará.

Mas não é muito mais legível do que por exemplo:

if (
    someComplicatedStuff()
    ||
    otherComplicatedStuff()
   )

3
Não estou interessado em combinar booleanos com um operador bit a bit. Isso não parece bem digitado para mim. Geralmente eu uso o que parece mais legível, a menos que esteja trabalhando em um nível muito baixo e a contagem de ciclos do processador.
— Formiga de

3
Usei especificamente b = b || otherComplicatedStuff();e @SargeBorsch fez uma edição para remover o SCE. Obrigado por me avisar sobre essa mudança @Ant.
— KIIV

14

1) Eu realmente perco o SCE todas as vezes? O compilador tem algum cenário permitido para "otimizá-lo" e ainda fornecer SCE?

Não acho que essa otimização seja permitida; especialmente OtherComplicatedFunctionCall()pode ter alguns efeitos colaterais.

2) Qual é a melhor prática nessa situação? É a única possibilidade (quando eu quiser SCE) de ter tudo o que preciso diretamente dentro se e "apenas formatar para ser o mais legível possível"?

Eu prefiro refatorá-lo em uma função ou variável com um nome descritivo; que irá preservar a avaliação de curto-circuito e a legibilidade:

bool getSomeResult() {
    return SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall();
}

...

if (getSomeResult())
{
    //do stuff
}

E conforme implementamos com getSomeResult()base em SomeComplicatedFunctionCall()e OtherComplicatedFunctionCall(), poderíamos decompor-os recursivamente se ainda forem complicados.


2
eu gosto disso porque você pode ganhar alguma legibilidade dando à função de wrapper um nome descritivo (embora provavelmente não getSomeResult), muitas outras respostas não adicionam nada de valor
— aw04

9

1) Eu realmente perco o SCE todas as vezes? O compilador tem algum cenário permitido para "otimizá-lo" e ainda fornecer SCE?

Não, não precisa, mas é aplicado de forma diferente:

if (SomeComplicatedFunctionCall() || OtherComplicatedFunctionCall())
{
    // do stuff
}

Aqui, o compilador nem mesmo rodará OtherComplicatedFunctionCall()se SomeComplicatedFunctionCall()retornar verdadeiro.

bool b1 = SomeComplicatedFunctionCall();
bool b2 = OtherComplicatedFunctionCall();

if (b1 || b2)
{
    //do stuff
}

Aqui, ambas as funções serão executadas porque devem ser armazenadas em b1e b2. Ff b1 == trueentão b2não será avaliado (SCE). Mas OtherComplicatedFunctionCall()já foi executado.

Se não b2for usado em nenhum outro lugar, o compilador pode ser inteligente o suficiente para embutir a chamada de função dentro do if se a função não tiver efeitos colaterais observáveis.

2) Qual é a melhor prática nessa situação? É a única possibilidade (quando eu quiser SCE) de ter tudo o que preciso diretamente dentro se e "apenas formatar para ser o mais legível possível"?

Depende. Você precisa OtherComplicatedFunctionCall() executar por causa dos efeitos colaterais ou o impacto no desempenho da função é mínimo, então você deve usar a segunda abordagem para facilitar a leitura. Caso contrário, siga o SCE na primeira abordagem.


8

Outra possibilidade de curto-circuito e ter as condições em um só lugar:

bool (* conditions [])()= {&a, &b, ...}; // list of conditions
bool conditionsHold = true;
for(int i= 0; i < sizeOf(conditions); i ++){
     if (!conditions[i]()){;
         conditionsHold = false;
         break;
     }
}
//conditionsHold is true if all conditions were met, otherwise false

Você poderia colocar o loop em uma função e deixar a função aceitar uma lista de condições e gerar um valor booleano.


1
@Erbureth Não, não são. Os elementos do array são ponteiros de função, eles não são executados até que as funções sejam chamadas no loop.
— Barmar de

Obrigado Barmar, mas fiz uma edição, Erbureth estava certo, antes da edição (achei que minha edição se propagaria visualmente de forma mais direta).
— levilime de

4

Muito estranho: você está falando sobre legibilidade quando ninguém menciona o uso de comentário dentro do código:

if (somecomplicated_function() || // let me explain what this function does
    someother_function())         // this function does something else
...

Além disso, sempre avanço minhas funções com alguns comentários, sobre a própria função, sobre sua entrada e saída, e às vezes coloco um exemplo, como você pode ver aqui:

/*---------------------------*/
/*! interpolates between values
* @param[in] X_axis : contains X-values
* @param[in] Y_axis : contains Y-values
* @param[in] value  : X-value, input to the interpolation process
* @return[out]      : the interpolated value
* @example          : interpolate([2,0],[3,2],2.4) -> 0.8
*/
int interpolate(std::vector<int>& X_axis, std::vector<int>& Y_axis, int value)

Obviamente, a formatação a ser usada para seus comentários pode depender de seu ambiente de desenvolvimento (Visual studio, JavaDoc no Eclipse, ...)

No que diz respeito ao SCE, presumo que você queira dizer o seguinte:

bool b1;
b1 = somecomplicated_function(); // let me explain what this function does
bool b2 = false;
if (!b1) {                       // SCE : if first function call is already true,
                                 // no need to spend resources executing second function.
  b2 = someother_function();     // this function does something else
}

if (b1 || b2) {
...
}

-7

A legibilidade é necessária se você trabalhar em uma empresa e seu código for lido por outra pessoa. Se você escreve um programa para você mesmo, depende de você se deseja sacrificar o desempenho em prol de um código compreensível.


23
Tendo em mente que "você daqui a seis meses" é definitivamente "outra pessoa", e "você amanhã" às vezes pode ser. Eu nunca sacrificaria a legibilidade pelo desempenho até que tivesse alguma evidência sólida de que havia um problema de desempenho.
— Martin Bonner apoia Monica em
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.