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?
relaxxx
fonte
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.

Horia Coman
fonte
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
  ) {
AmigoJack
fonte
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) { ... }
Algum cara programador
fonte
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.

SJuan76
fonte
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()
   )
KIIV
fonte
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.

Songyuanyao
fonte
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.

Frango Sombrero
fonte
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.

levilime
fonte
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) {
...
}
Dominique
fonte
-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.

br0lly
fonte
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