Como melhorar a lógica para verificar se 4 valores booleanos correspondem a alguns casos


118

Eu tenho quatro boolvalores:

bool bValue1;
bool bValue2;
bool bValue3;
bool bValue4;

Os valores aceitáveis ​​são:

         Scenario 1 | Scenario 2 | Scenario 3
bValue1: true       | true       | true
bValue2: true       | true       | false
bValue3: true       | true       | false
bValue4: true       | false      | false

Portanto, por exemplo, este cenário não é aceitável:

bValue1: false
bValue2: true
bValue3: true
bValue4: true

No momento, criei esta ifdeclaração para detectar cenários ruins:

if(((bValue4 && (!bValue3 || !bValue2 || !bValue1)) ||
   ((bValue3 && (!bValue2 || !bValue1)) ||
   (bValue2 && !bValue1) ||
   (!bValue1 && !bValue2 && !bValue3 && !bValue4))
{
    // There is some error
}

Essa lógica de instrução pode ser aprimorada / simplificada?


8
Eu usaria uma tabela em vez de uma ifdeclaração complexa . Além disso, como são sinalizadores booleanos, é possível modelar cada cenário como uma constante e comparar com ele.
Zdeslav Vojkovic

3
if (!((bValue1 && bValue2 && bValue3) || (bValue1 && !bValue2 && !bValue3 && !bValue4)))
Mc3

14
Quais são os cenários realmente? Muitas vezes, as coisas ficam muito mais simples se você apenas dar nomes coisas adequadas, por exemplobool scenario1 = bValue1 && bValue2 && bValue3 && bValue4;
idclev 463035818

6
Usando nomes significativos, você pode extrair cada condição complexa em um método e chamar esse método na condição if. Seria muito mais legível e sustentável. por exemplo, dê uma olhada no exemplo fornecido no link. refactoring.guru/decompose-conditional #
Hardik Modha

Respostas:


195

Eu apontaria para facilitar a leitura: você tem apenas 3 cenários, lide com eles com 3 ifs separados:

bool valid = false;
if (bValue1 && bValue2 && bValue3 && bValue4)
    valid = true; //scenario 1
else if (bValue1 && bValue2 && bValue3 && !bValue4)
    valid = true; //scenario 2
else if (bValue1 && !bValue2 && !bValue3 && !bValue4)
    valid = true; //scenario 3

Fácil de ler e depurar, IMHO. Além disso, você pode atribuir uma variável whichScenarioenquanto continua com o if.

Em apenas três cenários, eu não concordaria com algo como "se os 3 primeiros valores forem verdadeiros, posso evitar verificar o quarto valor": isso tornará seu código mais difícil de ler e manter.

Não é uma solução elegante talvez Certamente, mas neste caso está ok: fácil e legível.

Se sua lógica ficar mais complicada, jogue fora esse código e considere usar algo mais para armazenar diferentes cenários disponíveis (como sugere Zladeck).

Eu realmente amo a primeira sugestão dada nesta resposta : fácil de ler, não propenso a erros, sustentável

(Quase) fora do tópico:

Não escrevo muitas respostas aqui no StackOverflow. É realmente engraçado que a resposta acima aceita seja de longe a resposta mais apreciada da minha história (nunca tive mais do que 5 a 10 votos antes), enquanto na verdade não é o que eu normalmente acho que é a maneira "certa" de fazê-lo.

Mas a simplicidade é frequentemente "o caminho certo para fazê-lo", muitas pessoas parecem pensar isso e eu deveria pensar mais do que eu :)


1
Com certeza @hessamhedieh, tudo bem apenas para um pequeno número de cenários disponíveis. como eu disse, se as coisas ficam mais complicadas, melhor procurar outra coisa
Gian Paolo

4
Isso pode ser simplificado ainda mais, empilhando todas as condições no inicializador valide separando-as com ||, em vez de fazer a mutação validem blocos de instruções separados. Não posso colocar um exemplo no comentário, mas você pode alinhar verticalmente os ||operadores à esquerda para deixar isso muito claro; as condições individuais já estão entre parênteses tanto quanto precisam (para if), para que você não precise adicionar caracteres às expressões além do que já está lá.
Leushenko

1
@Leushenko, acho que misturar parênteses, && e || condições é bastante suscetível a erros (alguém em uma resposta diferente disse que houve um erro entre parênteses no código no OP, talvez tenha sido corrigido). O alinhamento adequado pode ajudar, com certeza. Mas qual é a vantagem? mais legível? mais fácil de manter? Acho que não. Apenas minha opinião, claro. E tenha certeza, eu realmente odeio ter muitos ifs no código.
Gian Paolo

3
Eu o envolveria de uma if($bValue1)maneira que sempre tem que ser verdade, tecnicamente permitindo algumas pequenas melhorias de desempenho (embora falemos de valores insignificantes aqui).
Martijn

2
FWIW: existem apenas dois cenários: os dois primeiros são o mesmo cenário e não dependem #bValue4
Dancrumb 3/18/18

123

Eu buscaria simplicidade e legibilidade.

bool scenario1 = bValue1 && bValue2 && bValue3 && bValue4;
bool scenario2 = bValue1 && bValue2 && bValue3 && !bValue4;
bool scenario3 = bValue1 && !bValue2 && !bValue3 && !bValue4;

if (scenario1 || scenario2 || scenario3) {
    // Do whatever.
}

Substitua os nomes dos cenários e os nomes dos sinalizadores por algo descritivo. Se isso faz sentido para o seu problema específico, você pode considerar esta alternativa:

bool scenario1or2 = bValue1 && bValue2 && bValue3;
bool scenario3 = bValue1 && !bValue2 && !bValue3 && !bValue4;

if (scenario1or2 || scenario3) {
    // Do whatever.
}

O importante aqui não é a lógica de predicado. Ele está descrevendo seu domínio e expressando claramente sua intenção. A chave aqui é fornecer bons nomes a todas as entradas e variáveis ​​intermediárias. Se você não conseguir encontrar bons nomes de variáveis, pode ser um sinal de que está descrevendo o problema da maneira errada.


3
+1 Isso é o que eu teria feito também. Assim como o @RedFilter aponta, e em contraste com a resposta aceita, isso é auto-documentado. Dar aos cenários seus próprios nomes em uma etapa separada é muito mais legível.
Andreas

105

Podemos usar um mapa de Karnaugh e reduzir seus cenários a uma equação lógica. Eu usei o resolvedor de mapas Online Karnaugh com circuito para 4 variáveis.

insira a descrição da imagem aqui

Isso produz:

insira a descrição da imagem aqui

Mudando A, B, C, Dpara bValue1, bValue2, bValue3, bValue4, isso não passa de:

bValue1 && bValue2 && bValue3 || bValue1 && !bValue2 && !bValue3 && !bValue4

Portanto, sua ifdeclaração se torna:

if(!(bValue1 && bValue2 && bValue3 || bValue1 && !bValue2 && !bValue3 && !bValue4))
{
    // There is some error
}
  • Os mapas de Karnaugh são particularmente úteis quando você tem muitas variáveis ​​e muitas condições que devem ser avaliadas true.
  • Depois de reduzir os truecenários a uma equação lógica, adicionar comentários relevantes indicando os truecenários é uma boa prática.

96
Embora tecnicamente correto, esse código requer muitos comentários para ser editado por outro desenvolvedor alguns meses depois.
Zdeslav Vojkovic

22
@ZdeslavVojkovic: Gostaria apenas de adicionar um comentário com a equação. //!(ABC + AB'C'D') (By K-Map logic). Esse seria um bom momento para o desenvolvedor aprender o K-Maps se ele ainda não os conhece.
PW

11
Concordo com isso, mas na IMO o problema é que ele não é mapeado claramente para o domínio do problema, ou seja, como cada condição é mapeada para um cenário específico, o que dificulta a alteração / extensão. O que acontece quando existem Ee Fcondições e 4 novos cenários? Quanto tempo leva para atualizar esta ifdeclaração corretamente? Como a revisão de código verifica se está ok ou não? O problema não está no lado técnico, mas no lado "comercial".
Zdeslav Vojkovic

7
Eu acho que você pode levar em consideração A: ABC + AB'C'D' = A(BC + B'C'D')(isso pode ser levado em consideração, A(B ^ C)'(C + D')embora eu tenha cuidado em chamar isso de 'simplificação').
Maciej Piechotka

28
@PW Esse comentário parece tão compreensível quanto o código e, portanto, é um pouco inútil. Um comentário melhor explicaria como você realmente criou essa equação, ou seja, que a declaração deve ser acionada para TTTT, TTTF e TFFF. Nesse ponto, você também pode escrever essas três condições no código e não precisar de uma explicação.
Bernhard Barker

58

A verdadeira questão aqui é: o que acontece quando outro desenvolvedor (ou mesmo autor) deve alterar esse código alguns meses depois.

Eu sugeriria modelar isso como sinalizadores de bits:

const int SCENARIO_1 = 0x0F; // 0b1111 if using c++14
const int SCENARIO_2 = 0x0E; // 0b1110
const int SCENARIO_3 = 0x08; // 0b1000

bool bValue1 = true;
bool bValue2 = false;
bool bValue3 = false;
bool bValue4 = false;

// boolean -> int conversion is covered by standard and produces 0/1
int scenario = bValue1 << 3 | bValue2 << 2 | bValue3 << 1 | bValue4;
bool match = scenario == SCENARIO_1 || scenario == SCENARIO_2 || scenario == SCENARIO_3;
std::cout << (match ? "ok" : "error");

Se houver muito mais cenários ou mais sinalizadores, uma abordagem de tabela é mais legível e extensível do que usar sinalizadores. O suporte a um novo cenário requer apenas mais uma linha na tabela.

int scenarios[3][4] = {
    {true, true, true, true},
    {true, true, true, false},
    {true, false, false, false},
};

int main()
{
  bool bValue1 = true;
  bool bValue2 = false;
  bool bValue3 = true;
  bool bValue4 = true;
  bool match = false;

  // depending on compiler, prefer std::size()/_countof instead of magic value of 4
  for (int i = 0; i < 4 && !match; ++i) {
    auto current = scenarios[i];
    match = bValue1 == current[0] && 
            bValue2 == current[1] && 
            bValue3 == current[2] && 
            bValue4 == current[3];
  }

  std::cout << (match ? "ok" : "error");
}

4
Não é o mais sustentável, mas definitivamente simplifica a condição if. Portanto, deixar alguns comentários em torno das operações bit a bit será uma necessidade absoluta aqui.
Adam Zahran

6
Na IMO, a tabela é a melhor abordagem, pois se adapta melhor a cenários e sinalizadores adicionais.
Zdeslav Vojkovic

Gosto da sua primeira solução, fácil de ler e aberta a modificações. Eu faria duas melhorias: 1: atribuir valores ao cenárioX com uma indicação explícita dos valores booleanos usados, por exemplo, SCENARIO_2 = true << 3 | true << 2 | true << 1 | false;2: evitar variáveis ​​SCENARIO_X e, em seguida, armazenar todos os cenários disponíveis em a <std::set<int>. Adicionar um cenário será apenas mySet.insert( true << 3 | false << 2 | true << 1 | false;um exagero para apenas três cenários, o OP aceitou a solução rápida, suja e fácil que sugeri na minha resposta.
Gian Paolo

4
Se você estiver usando C ++ 14 ou superior, sugiro usar literais binários para a primeira solução - 0b1111, 0b1110 e 0b1000 é muito mais claro. Provavelmente você também pode simplificar um pouco isso usando a biblioteca padrão ( std::find?).
Bernhard Barker

2
Acho que literais binários aqui seriam um requisito mínimo para limpar o primeiro código. Na sua forma atual, é completamente enigmático. Identificadores descritivos podem ajudar, mas não tenho certeza disso. De fato, as operações de bit para produzir o scenariovalor me parecem desnecessariamente sujeitas a erros.
21978 Konrad Rudolph

27

Minha resposta anterior já é a resposta aceita, adiciono aqui algo que acho que é legível, fácil e, neste caso, aberto a futuras modificações:

Começando com a resposta @ZdeslavVojkovic (que eu acho muito boa), eu vim com isso:

#include <iostream>
#include <set>

//using namespace std;

int GetScenarioInt(bool bValue1, bool bValue2, bool bValue3, bool bValue4)
{
    return bValue1 << 3 | bValue2 << 2 | bValue3 << 1 | bValue4;
}
bool IsValidScenario(bool bValue1, bool bValue2, bool bValue3, bool bValue4)
{
    std::set<int> validScenarios;
    validScenarios.insert(GetScenarioInt(true, true, true, true));
    validScenarios.insert(GetScenarioInt(true, true, true, false));
    validScenarios.insert(GetScenarioInt(true, false, false, false));

    int currentScenario = GetScenarioInt(bValue1, bValue2, bValue3, bValue4);

    return validScenarios.find(currentScenario) != validScenarios.end();
}

int main()
{
    std::cout << IsValidScenario(true, true, true, false) << "\n"; // expected = true;
    std::cout << IsValidScenario(true, true, false, false) << "\n"; // expected = false;

    return 0;
}

Veja aqui no trabalho

Bem, essa é a solução "elegante e de manutenção" (IMHO) que eu geralmente pretendo, mas realmente, para o caso do OP, minha resposta anterior "grupo de ifs" se encaixa melhor nos requisitos do OP, mesmo que não seja elegante nem sustentável.


Você sabe que sempre pode editar sua resposta anterior e fazer melhorias.
Andreas

20

Eu também gostaria de enviar uma outra abordagem.

Minha idéia é converter os bools em um número inteiro e depois comparar usando modelos variados:

unsigned bitmap_from_bools(bool b) {
    return b;
}
template<typename... args>
unsigned bitmap_from_bools(bool b, args... pack) {
    return (bitmap_from_bools(b) << sizeof...(pack)) | bitmap_from_bools(pack...);
}

int main() {
    bool bValue1;
    bool bValue2;
    bool bValue3;
    bool bValue4;

    unsigned summary = bitmap_from_bools(bValue1, bValue2, bValue3, bValue4);

    if (summary != 0b1111u && summary != 0b1110u && summary != 0b1000u) {
        //bad scenario
    }
}

Observe como este sistema pode suportar até 32 bools como entrada. substituir o unsignedcom unsigned long long(ou uint64_t) aumenta o suporte para 64 casos. Se você não gostar do if (summary != 0b1111u && summary != 0b1110u && summary != 0b1000u), também poderá usar outro método de modelo variável:

bool equals_any(unsigned target, unsigned compare) {
    return target == compare;
}
template<typename... args>
bool equals_any(unsigned target, unsigned compare, args... compare_pack) {
    return equals_any(target, compare) ? true : equals_any(target, compare_pack...);
}

int main() {
    bool bValue1;
    bool bValue2;
    bool bValue3;
    bool bValue4;

    unsigned summary = bitmap_from_bools(bValue1, bValue2, bValue3, bValue4);

    if (!equals_any(summary, 0b1111u, 0b1110u, 0b1000u)) {
        //bad scenario
    }
}

2
Eu amo essa abordagem, exceto pelo nome da função principal: "de bool ... para quê ?" - Por que não explicitamente bitmap_from_bools, ou bools_to_bitmap?
21978 Konrad Rudolph

sim @KonradRudolph, eu não conseguia pensar em um nome melhor, exceto talvez bools_to_unsigned. Bitmap é uma boa palavra-chave; editado.
Stack Danny

Eu acho que você quer summary!= 0b1111u &&.... a != b || a != cé sempre verdadeiro seb != c
MooseBoys

17

Aqui está uma versão simplificada:

if (bValue1 && (bValue2 == bValue3) && (bValue2 || !bValue4)) {
    // acceptable
} else {
    // not acceptable
}

Observe, é claro, que esta solução é mais ofuscada que a original, seu significado pode ser mais difícil de entender.


Atualização: os MSalters nos comentários encontraram uma expressão ainda mais simples:

if (bValue1&&(bValue2==bValue3)&&(bValue2>=bValue4)) ...

1
Sim, mas difícil de entender. Mas obrigado pela sugestão.
precisa saber é o seguinte

Comparei a capacidade dos compiladores de simplificar a expressão com a sua simplificação como referência: explorador do compilador . O gcc não encontrou sua versão ideal, mas sua solução ainda é boa. Clang e MSVC parecem não executar nenhuma simplificação de expressão booleana.
Oliv

1
@AndrewTruckle: note que se você precisava de uma versão mais legível, diga-o. Você disse "simplificado", mas aceita uma versão ainda mais detalhada do que a original.
geza

1
simpleé realmente um termo vago. Muitas pessoas entendem isso neste contexto como mais simples para o desenvolvedor entender e não para o compilador gerar código; portanto, mais detalhado pode realmente ser mais simples.
Zdeslav Vojkovic

1
@IsmaelMiguel: quando uma fórmula lógica é otimizada para o número de termos, o significado original geralmente é perdido. Mas pode-se colocar um comentário a respeito, para que fique claro o que ele faz. Mesmo, para a resposta aceita, um comentário não faria mal.
geza

12

Considere traduzir suas tabelas o mais diretamente possível em seu programa. Dirija o programa com base fora da mesa, em vez de imitá-lo com lógica.

template<class T0>
auto is_any_of( T0 const& t0, std::initializer_list<T0> il ) {
  for (auto&& x:il)
    if (x==t0) return true;
  return false;
}

agora

if (is_any_of(
  std::make_tuple(bValue1, bValue2, bValue3, bValue4),
  {
    {true, true, true, true},
    {true, true, true, false},
    {true, false, false, false}
  }
))

isso diretamente possível codifica sua tabela verdade no compilador.

Exemplo ao vivo .

Você também pode usar std::any_ofdiretamente:

using entry = std::array<bool, 4>;
constexpr entry acceptable[] = 
  {
    {true, true, true, true},
    {true, true, true, false},
    {true, false, false, false}
  };
if (std::any_of( begin(acceptable), end(acceptable), [&](auto&&x){
  return entry{bValue1, bValue2, bValue3, bValue4} == x;
}) {
}

o compilador pode incorporar o código e eliminar qualquer iteração e criar sua própria lógica para você. Enquanto isso, seu código reflete exatamente como você se ocultou do problema.


A primeira versão é tão fácil de ler e tão fácil de manter que eu realmente gosto. O segundo é mais difícil de ler, pelo menos para mim, e requer um nível de habilidade em C ++ talvez acima da média, certamente acima do meu. Não é algo que todos possam escrever. Acabou de aprender o somethin novo, graças
Gian Paolo

11

Estou apenas fornecendo minha resposta aqui, como nos comentários que alguém sugeriu para mostrar minha solução. Quero agradecer a todos por suas idéias.

No final, optei por adicionar três novos booleanmétodos de "cenário" :

bool CChristianLifeMinistryValidationDlg::IsFirstWeekStudentItems(CChristianLifeMinistryEntry *pEntry)
{
    return (INCLUDE_ITEM1(pEntry) && 
           !INCLUDE_ITEM2(pEntry) && 
           !INCLUDE_ITEM3(pEntry) && 
           !INCLUDE_ITEM4(pEntry));
}

bool CChristianLifeMinistryValidationDlg::IsSecondWeekStudentItems(CChristianLifeMinistryEntry *pEntry)
{
    return (INCLUDE_ITEM1(pEntry) &&
            INCLUDE_ITEM2(pEntry) &&
            INCLUDE_ITEM3(pEntry) &&
            INCLUDE_ITEM4(pEntry));
}

bool CChristianLifeMinistryValidationDlg::IsOtherWeekStudentItems(CChristianLifeMinistryEntry *pEntry)
{
    return (INCLUDE_ITEM1(pEntry) && 
            INCLUDE_ITEM2(pEntry) && 
            INCLUDE_ITEM3(pEntry) && 
           !INCLUDE_ITEM4(pEntry));
}

Então, eu pude aplicar aqueles a minha rotina de validação como esta:

if (!IsFirstWeekStudentItems(pEntry) && !IsSecondWeekStudentItems(pEntry) && !IsOtherWeekStudentItems(pEntry))
{
    ; Error
}

Na minha aplicação ao vivo, os 4 valores bool são realmente extraídos de um DWORDque possui 4 valores codificados.

Mais uma vez obrigado a todos.


1
Obrigado por compartilhar a solução. :) Na verdade, é melhor que o complexo, se as condições forem um inferno. Talvez você ainda possa nomear INCLUDE_ITEM1etc de uma maneira melhor e tudo seja bom. :)
Hardik Modha

1
@HardikModha Bem, tecnicamente, eles são "itens do aluno" e a bandeira é para indicar se eles devem ser "incluídos". Então, acho que o nome, embora pareça genérico, é realmente significativo nesse contexto. :)
Andrew Truckle

11

Não estou vendo nenhuma resposta dizendo o nome dos cenários, embora a solução do OP faça exatamente isso.

Para mim, é melhor encapsular o comentário do que cada cenário faz em um nome de variável ou nome de função. É mais provável que você ignore um comentário do que um nome e, se sua lógica mudar no futuro, é mais provável que você mude um nome do que um comentário. Você não pode refatorar um comentário.

Se você planeja reutilizar esses cenários fora de sua função (ou desejar), crie uma função que diga o que avalia ( constexpr/ noexceptopcional, mas recomendado):

constexpr bool IsScenario1(bool b1, bool b2, bool b3, bool b4) noexcept
{ return b1 && b2 && b3 && b4; }

constexpr bool IsScenario2(bool b1, bool b2, bool b3, bool b4) noexcept
{ return b1 && b2 && b3 && !b4; }

constexpr bool IsScenario3(bool b1, bool b2, bool b3, bool b4) noexcept
{ return b1 && !b2 && !b3 && !b4; }

Faça esses métodos de classe, se possível (como na solução do OP). Você pode usar variáveis ​​dentro de sua função se não achar que reutilizará a lógica:

const auto is_scenario_1 = bValue1 && bValue2 && bValue3 && bValue4;
const auto is_scenario_2 = bvalue1 && bvalue2 && bValue3 && !bValue4;
const auto is_scenario_3 = bValue1 && !bValue2 && !bValue3 && !bValue4;

O compilador provavelmente resolverá que, se bValue1 for falso, todos os cenários serão falsos. Não se preocupe em torná-lo rápido, apenas correto e legível. Se você criar um perfil do seu código e achar que isso é um gargalo, porque o compilador gerou um código abaixo do ideal em -O2 ou superior, tente reescrevê-lo.


Gosto disso um pouco mais do que a solução (já boa) de Gian Paolo: evita o fluxo de controle e o uso de uma variável que é sobrescrita - estilo mais funcional.
Dirk Herrmann

9

Maneira de AC / C ++

bool scenario[3][4] = {{true, true, true, true}, 
                        {true, true, true, false}, 
                        {true, false, false, false}};

bool CheckScenario(bool bValue1, bool bValue2, bool bValue3, bool bValue4)
{
    bool temp[] = {bValue1, bValue2, bValue3, bValue4};
    for(int i = 0 ; i < sizeof(scenario) / sizeof(scenario[0]); i++)
    {
        if(memcmp(temp, scenario[i], sizeof(temp)) == 0)
            return true;
    }
    return false;
}

Essa abordagem é escalável, como se o número de condições válidas aumentasse, você adiciona mais facilmente à lista de cenários.


Eu tenho certeza que isso está errado, no entanto. Ele pressupõe que o compilador use apenas uma única representação binária para true. Um compilador que usa "qualquer coisa que não seja zero é verdadeiro" faz com que esse código falhe. Observe que truedeve ser convertido para 1, ele simplesmente não precisa ser armazenado como tal.
precisa saber é o seguinte

@MSalters, tnx, entendi seu ponto de vista e sei que, tipo 2 is not equal to true but evaluates to true, meu código não força int 1 = truee funciona desde que todas as verdadeiras sejam convertidas no mesmo valor int, então, aqui está a minha pergunta: Por que o compilador deve agir aleatoriamente ao converter fiel ao int subjacente, você pode elaborar mais?
hessam hedieh

Executar um memcmppara testar condições booleanas não é a maneira C ++, e duvido que seja uma maneira C estabelecida.
21978 Konrad Rudolph

@hessamhedieh: O problema em sua lógica é "converter true para int subjacente". Não é assim que os compiladores funcionam.
MSalters

Seu código aumenta a complexidade de O (1) para O (n). Não é um caminho a percorrer em nenhum idioma - deixe de lado o C / C ++.
Mabel

9

É fácil perceber que os dois primeiros cenários são semelhantes - eles compartilham a maioria das condições. Se você quiser selecionar em qual cenário está no momento, escreva-o assim (é uma solução modificada do @ gian-paolo ):

bool valid = false;
if(bValue1 && bValue2 && bValue3)
{
    if (bValue4)
        valid = true; //scenario 1
    else if (!bValue4)
        valid = true; //scenario 2
}
else if (bValue1 && !bValue2 && !bValue3 && !bValue4)
    valid = true; //scenario 3

Indo além, você pode notar que o primeiro booleano precisa ser sempre verdadeiro, o que é uma condição de entrada, para que você possa terminar com:

bool valid = false;
if(bValue1)
{
    if(bValue2 && bValue3)
    {
        if (bValue4)
            valid = true; //scenario 1
        else if (!bValue4)
            valid = true; //scenario 2
    }
    else if (!bValue2 && !bValue3 && !bValue4)
        valid = true; //scenario 3
}

Ainda mais, agora você pode ver claramente que bValue2 e bValue3 estão um pouco conectados - você pode extrair seu estado para algumas funções ou variáveis ​​externas com nome mais apropriado (embora isso nem sempre seja fácil ou apropriado):

bool valid = false;
if(bValue1)
{
    bool bValue1and2 = bValue1 && bValue2;
    bool notBValue1and2 = !bValue2 && !bValue3;
    if(bValue1and2)
    {
        if (bValue4)
            valid = true; //scenario 1
        else if (!bValue4)
            valid = true; //scenario 2
    }
    else if (notBValue1and2 && !bValue4)
        valid = true; //scenario 3
}

Fazer dessa maneira tem algumas vantagens e desvantagens:

  • as condições são menores, por isso é mais fácil argumentar sobre elas,
  • é mais fácil renomear para tornar essas condições mais compreensíveis,
  • mas eles precisam entender o escopo,
  • além disso, é mais rígido

Se você prevê que haverá mudanças na lógica acima, use uma abordagem mais direta, como apresentado por @ gian-paolo .

Caso contrário, se essas condições estiverem bem estabelecidas e forem "regras sólidas" que nunca mudarão, considere meu último trecho de código.


7

Conforme sugerido por mch, você pode fazer:

if(!((bValue1 && bValue2 && bValue3) || 
  (bValue1 && !bValue2 && !bValue3 && !bValue4))
)

onde a primeira linha cobre os dois primeiros casos bons e a segunda linha cobre o último.

Live Demo, onde eu brinquei e passa seus casos.


7

Uma ligeira variação na boa resposta de @ GianPaolo, que alguns podem achar mais fácil de ler:

bool any_of_three_scenarios(bool v1, bool v2, bool v3, bool v4)
{
  return (v1 &&  v2 &&  v3 &&  v4)  // scenario 1
      || (v1 &&  v2 &&  v3 && !v4)  // scenario 2
      || (v1 && !v2 && !v3 && !v4); // scenario 3
}

if (any_of_three_scenarios(bValue1,bValue2,bValue3,bValue4))
{
  // ...
}

7

Toda resposta é excessivamente complexa e difícil de ler. A melhor solução para isso é uma switch()declaração. É legível e simplifica a adição / modificação de casos adicionais. Compiladores também são bons em otimizar switch()declarações.

switch( (bValue4 << 3) | (bValue3 << 2) | (bValue2 << 1) | (bValue1) )
{
    case 0b1111:
        // scenario 1
        break;

    case 0b0111:
        // scenario 2
        break;

    case 0b0001:
        // scenario 3
        break;

    default:
        // fault condition
        break;
}

É claro que você pode usar constantes e OU juntas nas caseinstruções para facilitar a leitura.


Sendo um programador C antigo, eu definiria uma macro "PackBools" e a usaria tanto para a "opção (PackBools (a, b, c, d))" quanto para os casos, por exemplo, diretamente "case PackBools (true , true ...) "ou defina-as como constantes locais.eg" const unsigned int scenery1 = PackBools (true, true ...); "
Simon F

6

Eu também usaria variáveis ​​de atalho para maior clareza. Como observado anteriormente, o cenário 1 é igual ao cenário 2, porque o valor de bValue4 não influencia a verdade desses dois cenários.

bool MAJORLY_TRUE=bValue1 && bValue2 && bValue3
bool MAJORLY_FALSE=!(bValue2 || bValue3 || bValue4)

então sua expressão aparece:

if (MAJORLY_TRUE || (bValue1 && MAJORLY_FALSE))
{
     // do something
}
else
{
    // There is some error
}

Dar nomes significativos às variáveis ​​MAJORTRUE e MAJORFALSE (assim como, na verdade, ao bValue * vars) ajudaria muito na legibilidade e manutenção.


6

Concentre-se na legibilidade do problema, não na declaração "if" específica.

Embora isso produza mais linhas de código, alguns podem considerar um exagero ou desnecessário. Eu sugeriria que abstrair seus cenários dos booleanos específicos é a melhor maneira de manter a legibilidade.

Ao dividir as coisas em classes (fique à vontade para usar apenas funções ou qualquer outra ferramenta que você preferir) com nomes compreensíveis - podemos muito mais facilmente mostrar os significados por trás de cada cenário. Mais importante, em um sistema com muitas partes móveis - é mais fácil manter e ingressar nos sistemas existentes (novamente, apesar da quantidade de código extra que é invocado).

#include <iostream>
#include <vector>
using namespace std;

// These values would likely not come from a single struct in real life
// Instead, they may be references to other booleans in other systems
struct Values
{
    bool bValue1; // These would be given better names in reality
    bool bValue2; // e.g. bDidTheCarCatchFire
    bool bValue3; // and bDidTheWindshieldFallOff
    bool bValue4;
};

class Scenario
{
public:
    Scenario(Values& values)
    : mValues(values) {}

    virtual operator bool() = 0;

protected:
    Values& mValues;    
};

// Names as examples of things that describe your "scenarios" more effectively
class Scenario1_TheCarWasNotDamagedAtAll : public Scenario
{
public:
    Scenario1_TheCarWasNotDamagedAtAll(Values& values) : Scenario(values) {}

    virtual operator bool()
    {
        return mValues.bValue1
        && mValues.bValue2
        && mValues.bValue3
        && mValues.bValue4;
    }
};

class Scenario2_TheCarBreaksDownButDidntGoOnFire : public Scenario
{
public:
    Scenario2_TheCarBreaksDownButDidntGoOnFire(Values& values) : Scenario(values) {}

    virtual operator bool()
    {
        return mValues.bValue1
        && mValues.bValue2
        && mValues.bValue3
        && !mValues.bValue4;
    }   
};

class Scenario3_TheCarWasCompletelyWreckedAndFireEverywhere : public Scenario
{
public:
    Scenario3_TheCarWasCompletelyWreckedAndFireEverywhere(Values& values) : Scenario(values) {}

    virtual operator bool()
    {
        return mValues.bValue1
        && !mValues.bValue2
        && !mValues.bValue3
        && !mValues.bValue4;
    }   
};

Scenario* findMatchingScenario(std::vector<Scenario*>& scenarios)
{
    for(std::vector<Scenario*>::iterator it = scenarios.begin(); it != scenarios.end(); it++)
    {
        if (**it)
        {
            return *it;
        }
    }
    return NULL;
}

int main() {
    Values values = {true, true, true, true};
    std::vector<Scenario*> scenarios = {
        new Scenario1_TheCarWasNotDamagedAtAll(values),
        new Scenario2_TheCarBreaksDownButDidntGoOnFire(values),
        new Scenario3_TheCarWasCompletelyWreckedAndFireEverywhere(values)
    };

    Scenario* matchingScenario = findMatchingScenario(scenarios);

    if(matchingScenario)
    {
        std::cout << matchingScenario << " was a match" << std::endl;
    }
    else
    {
        std::cout << "No match" << std::endl;
    }

    // your code goes here
    return 0;
}

5
Em algum momento, a verbosidade começa a prejudicar a legibilidade. Eu acho que isso vai longe demais.
jollyjoker

2
@JollyJoker Na verdade, eu concordo nessa situação específica - no entanto, meu pressentimento pela maneira como o OP nomeou tudo extremamente genericamente é que o código "real" provavelmente é muito mais complexo do que o exemplo que eles deram. Realmente, eu só queria apresentar essa alternativa, pois é assim que eu a estruturaria para algo muito mais complexo / envolvido. Mas você está certo - para um exemplo específico de OP, é excessivamente detalhado e piora a situação.

5

Depende do que eles representam.

Por exemplo, se 1 é uma chave e 2 e 3 são duas pessoas que devem concordar (exceto se concordarem NOTque precisam de uma terceira pessoa - 4 - para confirmar), o mais legível pode ser:

1 &&
    (
        (2 && 3)   
        || 
        ((!2 && !3) && !4)
    )

pelo pedido popular:

Key &&
    (
        (Alice && Bob)   
        || 
        ((!Alice && !Bob) && !Charlie)
    )

2
Você pode estar certo, mas usar números para ilustrar seu ponto prejudica sua resposta. Tente usar nomes descritivos.
Jxh

1
@jxh Esses são os números OP utilizados. Acabei de remover o bValue.
Ispiro

@ jxh Espero que esteja melhor agora.
Ispiro

4

A operação bit a bit parece muito limpa e compreensível.

int bitwise = (bValue4 << 3) | (bValue3 << 2) | (bValue2 << 1) | (bValue1);
if (bitwise == 0b1111 || bitwise == 0b0111 || bitwise == 0b0001)
{
    //satisfying condition
}

1
A comparação bit a bit parece legível para mim. A composição, por outro lado, parece artificial.
Xtofl

3

Estou denotando a, b, c, d para maior clareza, e A, B, C, D para complementos

bValue1 = a (!A)
bValue2 = b (!B)
bValue3 = c (!C)
bValue4 = d (!D)

Equação

1 = abcd + abcD + aBCD
  = a (bcd + bcD + BCD)
  = a (bc + BCD)
  = a (bcd + D (b ^C))

Use qualquer equação que combina com você.


3
If (!bValue1 || (bValue2 != bValue3) || (!bValue4 && bValue2))
{
// you have a problem
}
  • b1 deve sempre ser verdadeiro
  • b2 deve sempre ser igual a b3
  • e b4 não pode ser falso se b2 (e b3) forem verdadeiros

simples


3

Apenas uma preferência pessoal sobre a resposta aceita, mas eu escreveria:

bool valid = false;
// scenario 1
valid = valid || (bValue1 && bValue2 && bValue3 && bValue4);
// scenario 2
valid = valid || (bValue1 && bValue2 && bValue3 && !bValue4);
// scenario 3
valid = valid || (bValue1 && !bValue2 && !bValue3 && !bValue4);

2

Primeiro, supondo que você possa modificar apenas a verificação do cenário, eu me concentraria na legibilidade e apenas agruparia a verificação em uma função para que você possa simplesmente chamar if(ScenarioA()).


Agora, supondo que você realmente queira / precise otimizar isso, eu recomendaria converter os booleanos firmemente vinculados em números inteiros constantes e usar operadores de bits neles

public class Options {
  public const bool A = 2; // 0001
  public const bool B = 4; // 0010
  public const bool C = 16;// 0100
  public const bool D = 32;// 1000
//public const bool N = 2^n; (up to n=32)
}

...

public isScenario3(int options) {
  int s3 = Options.A | Options.B | Options.C;
  // for true if only s3 options are set
  return options == s3;
  // for true if s3 options are set
  // return options & s3 == s3
}

Isso torna a expressão dos cenários tão fácil quanto listar o que faz parte dela, permite que você use uma instrução switch para ir para a condição correta e confunda outros desenvolvedores que nunca viram isso antes. (C # RegexOptions usa esse padrão para definir sinalizadores, não sei se existe um exemplo de biblioteca c ++)


Na verdade, não estou usando quatro valores bool, mas um DWORD com quatro BOOLS incorporados. Tarde demais para mudar isso agora. Mas obrigado pela sua sugestão.
Andrew Truckle

2

ifS aninhados podem ser mais fáceis de ler para algumas pessoas. Aqui está a minha versão

bool check(int bValue1, int bValue2, int bValue3, int bValue4)
{
  if (bValue1)
  {
    if (bValue2)
    {
      // scenario 1-2
      return bValue3;
    }
    else
    {
      // scenario 3
      return !bValue3 && !bValue4;
    }
  }

  return false;
}

Pessoalmente, eu normalmente evitaria aninhar declarações se possível. Embora este caso seja agradável e legível, uma vez que novas possibilidades são adicionadas, o aninhamento pode se tornar muito difícil de ler. Mas se os cenários nunca mudam, é definitivamente uma solução agradável e legível.
Dnomyar96

@ Dnomyar96 eu concordo. Eu pessoalmente evito ifs aninhados também. Às vezes, se a lógica é complicada, é mais fácil para mim entender a lógica, dividindo-a em pedaços. Por exemplo, depois de inserir o bValue1bloco, você poderá tratar tudo nele como uma nova página nova em seu processo mental. Aposto que a maneira de abordar o problema pode ser algo muito pessoal ou mesmo cultural.
Sardok

1

Várias respostas corretas foram dadas a essa pergunta, mas eu adotaria uma visão diferente: se o código parecer muito complicado, algo não está certo . O código será difícil de depurar e provavelmente será "somente para uso único".

Na vida real, quando encontramos uma situação como esta:

         Scenario 1 | Scenario 2 | Scenario 3
bValue1: true       | true       | true
bValue2: true       | true       | false
bValue3: true       | true       | false
bValue4: true       | false      | false

Quando quatro estados são conectados por um padrão tão preciso, estamos lidando com a configuração de alguma "entidade" em nosso modelo .

Uma metáfora extrema é como descreveríamos um "ser humano" em um modelo, se não estivéssemos cientes de sua existência como entidades unitárias com componentes conectados em graus específicos de liberdade: teríamos que descrever estados independentes de "torso", "braços", "pernas" e "cabeça", o que tornaria complicado entender o sistema descrito. Um resultado imediato seria expressões booleanas artificialmente complicadas.

Obviamente, a maneira de reduzir a complexidade é a abstração e uma ferramenta de escolha em c ++ é o paradigma do objeto. .

Portanto, a pergunta é: por que existe esse padrão? O que é isso e o que representa?

Como não sabemos a resposta, podemos recorrer a uma abstração matemática: a matriz : temos três cenários, cada um dos quais agora é uma matriz.

                0   1   2   3
Scenario 1:     T   T   T   T
Scenario 2:     T   T   T   F
Scenario 3:     T   F   F   F

Nesse ponto, você tem sua configuração inicial. como uma matriz. Por exemplo, std::arraytem um operador de igualdade:

Nesse momento, sua sintaxe se torna:

if( myarray == scenario1 ) {
  // arrays contents are the same

} 
else if ( myarray == scenario2 ) {
  // arrays contents are the same

} 

else if ( myarray == scenario3 ) {
  // arrays contents are the same

} 
else {
  // not the same

}

Assim como a resposta de Gian Paolo, é curta, clara e facilmente verificável / depurável. Nesse caso, delegamos os detalhes das expressões booleanas no compilador.


1

Você não precisará se preocupar com combinações inválidas de sinalizadores booleanos se livrar dos sinalizadores booleanos.

Os valores aceitáveis ​​são:

         Scenario 1 | Scenario 2 | Scenario 3
bValue1: true       | true       | true
bValue2: true       | true       | false
bValue3: true       | true       | false
bValue4: true       | false      | false

Você tem claramente três estados (cenários). Seria melhor modelar isso e derivar as propriedades booleanas desses estados, e não o contrário.

enum State
{
    scenario1,
    scenario2,
    scenario3,
};

inline bool isValue1(State s)
{
    // (Well, this is kind of silly.  Do you really need this flag?)
    return true;
}

inline bool isValue2(State s)
{
    switch (s)
    {
        case scenario1:
        case scenario2:
            return true;
        case scenario3:
            return false;
    }
}

inline bool isValue3(State s)
{
    // (This is silly too.  Do you really need this flag?)
    return isValue2(s);
}

inline bool isValue4(State s)
{
    switch (s)
    {
        case scenario1:
            return true;
        case scenario2:
        case scenario3:
            return false;
    }
}

Definitivamente, este é mais código do que na resposta de Gian Paolo , mas, dependendo da sua situação, isso pode ser muito mais sustentável:

  • Há um conjunto central de funções para modificar se propriedades ou cenários booleanos adicionais forem adicionados.
    • Adicionar propriedades requer adicionar apenas uma única função.
    • Ao adicionar um cenário, habilitar avisos do compilador sobre não manipulados enum casos switchnão tratados nas instruções capturará getters de propriedades que não lidam com esse cenário.
  • Se você precisar modificar as propriedades booleanas dinamicamente, não precisará revalidar suas combinações em todos os lugares. Em vez de alternar sinalizadores booleanos individuais (o que poderia resultar em combinações inválidas de sinalizadores), você teria uma máquina de estado que transita de um cenário para outro.

Essa abordagem também tem o benefício colateral de ser muito eficiente.


0

Meus 2 centavos: declarar uma soma variável (inteiro) para que

if(bValue1)
{
  sum=sum+1;
}
if(bValue2)
{
  sum=sum+2;
}
if(bValue3)
{
  sum=sum+4;
}
if(bValue4)
{
  sum=sum+8;
}

Verifique a soma de acordo com as condições que você deseja e é isso. Dessa forma, você poderá adicionar facilmente mais condições no futuro, mantendo a leitura bastante simples.


0

A resposta aceita é boa quando você tem apenas 3 casos e a lógica de cada um é simples.

Mas se a lógica de cada caso for mais complicada ou se houver muitos outros casos, uma opção muito melhor é usar o padrão de design da cadeia de responsabilidades .

Você cria um BaseValidatorque contém uma referência a BaseValidatore um método validatee um método para chamar a validação no validador referenciado.

class BaseValidator {
    BaseValidator* nextValidator;

    public:
    BaseValidator() {
        nextValidator = 0;
    }

    void link(BaseValidator validator) {
        if (nextValidator) {
            nextValidator->link(validator);
        } else {
            nextValidator = validator;
        }
    }

    bool callLinkedValidator(bool v1, bool v2, bool v3, bool v4) {
        if (nextValidator) {
            return nextValidator->validate(v1, v2, v3, v4);
        }

        return false;
    }

    virtual bool validate(bool v1, bool v2, bool v3, bool v4) {
        return false;
    }
}

Em seguida, você cria um número de subclasses que herdam BaseValidator, substituindo o validatemétodo pela lógica necessária para cada validador.

class Validator1: public BaseValidator {
    public:
    bool validate(bool v1, bool v2, bool v3, bool v4) {
        if (v1 && v2 && v3 && v4) {
            return true;
        }

        return nextValidator->callLinkedValidator(v1, v2, v3, v4);
    }
}

Em seguida, usá-lo é simples, instancie cada um dos seus validadores e defina cada um deles como a raiz dos outros:

Validator1 firstValidator = new Validator1();
Validator2 secondValidator = new Validator2();
Validator3 thirdValidator = new Validator3();
firstValidator.link(secondValidator);
firstValidator.link(thirdValidator);
if (firstValidator.validate(value1, value2, value3, value4)) { ... }

Em essência, cada caso de validação possui sua própria classe, responsável por (a) determinar se a validação corresponde àquela caso, e (b) enviar a validação para outra pessoa na cadeia, se não for.

Observe que eu não estou familiarizado com C ++. Eu tentei combinar a sintaxe de alguns exemplos que encontrei online, mas se isso não funcionar, trate-o mais como pseudocódigo. Eu também tenho um exemplo completo de trabalho em Python abaixo que pode ser usado como base, se preferir.

class BaseValidator:
    def __init__(self):
        self.nextValidator = 0

    def link(self, validator):
        if (self.nextValidator):
            self.nextValidator.link(validator)
        else:
            self.nextValidator = validator

    def callLinkedValidator(self, v1, v2, v3, v4):
        if (self.nextValidator):
            return self.nextValidator.validate(v1, v2, v3, v4)

        return False

    def validate(self, v1, v2, v3, v4):
        return False

class Validator1(BaseValidator):
    def validate(self, v1, v2, v3, v4):
        if (v1 and v2 and v3 and v4):
            return True
        return self.callLinkedValidator(v1, v2, v3, v4)

class Validator2(BaseValidator):
    def validate(self, v1, v2, v3, v4):
        if (v1 and v2 and v3 and not v4):
            return True
        return self.callLinkedValidator(v1, v2, v3, v4)

class Validator3(BaseValidator):
    def validate(self, v1, v2, v3, v4):
        if (v1 and not v2 and not v3 and not v4):
            return True
        return self.callLinkedValidator(v1, v2, v3, v4)

firstValidator = Validator1()
secondValidator = Validator2()
thirdValidator = Validator3()
firstValidator.link(secondValidator)
firstValidator.link(thirdValidator)
print(firstValidator.validate(False, False, True, False))

Novamente, você pode encontrar esse exagero no seu exemplo específico, mas ele cria um código muito mais limpo se você acabar com um conjunto de casos muito mais complicado que precisa ser atendido.


-2

Uma abordagem simples é encontrar a resposta que você considera aceitável.

Sim = (boolean1 && boolean2 && boolean3 && boolean4) + + ...

Agora, se possível, simplifique a equação usando álgebra booleana.

como neste caso, aceitável1 e 2 combinam-se com (boolean1 && boolean2 && boolean3).

Portanto, a resposta final é:

(boolean1 && boolean2 && boolean3) || 
((boolean1 && !boolean2 && !boolean3 && !boolean4)

-3

use o campo de bits :

unoin {
  struct {
    bool b1: 1;
    bool b2: 1;
    bool b3: 1;
    bool b4: 1;
  } b;
  int i;
} u;

// set:
u.b.b1=true;
...

// test
if (u.i == 0x0f) {...}
if (u.i == 0x0e) {...}
if (u.i == 0x08) {...}

PS :

É uma grande pena para os CPP. Mas, UB não é minha preocupação, verifique em http://coliru.stacked-crooked.com/a/2b556abfc28574a1 .


2
Isso causa UB devido ao acesso a um campo de união inativo.
precisa saber é o seguinte

Formalmente, é UB em C ++, você não pode definir um membro da união e ler de outro. Tecnicamente, seria melhor implementar getters \ setters modelados para bits de valor integral.
Swift - sexta-feira Pie

Eu acho que o comportamento mudaria para Definido para Implementação se alguém convertesse o endereço do sindicato em um unsigned char*, embora eu ache que simplesmente usar algo como ((((flag4 <<1) | flag3) << 1) | flag2) << 1) | flag1provavelmente seria mais eficiente.
Supercat
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.