Come migliorare la logica per verificare se 4 valori booleani corrispondono ad alcuni casi


118

Ho quattro boolvalori:

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

I valori accettabili sono:

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

Quindi, ad esempio, questo scenario non è accettabile:

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

Al momento ho escogitato questa ifaffermazione per rilevare cattivi scenari:

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

Questa logica di affermazione può essere migliorata / semplificata?


8
Userei una tabella invece di una ifdichiarazione complessa . Inoltre, poiché si tratta di flag booleani, è possibile modellare ogni scenario come una costante e confrontarlo.
Zdeslav Vojkovic

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

14
quali sono gli scenari in realtà? Spesso le cose diventano molto più semplici se si danno solo nomi propri alle cose, ad esempiobool scenario1 = bValue1 && bValue2 && bValue3 && bValue4;
idclev 463035818

6
Utilizzando nomi significativi, è possibile estrarre ogni condizione complessa in un metodo e chiamare quel metodo in condizione if. Sarebbe molto più leggibile e gestibile. es. Dai un'occhiata all'esempio fornito nel link. refactoring.guru/decompose-conditional
Hardik Modha

Risposte:


195

Io mirerei alla leggibilità: hai solo 3 scenari, affrontali con 3 se separati:

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

Facile da leggere ed eseguire il debug, IMHO. Inoltre, puoi assegnare una variabile whichScenariomentre procedi con il file if.

Con solo 3 scenari, non andrei con qualcosa del tipo "se i primi 3 valori sono veri posso evitare di controllare il quarto valore": renderà il tuo codice più difficile da leggere e mantenere.

Non una soluzione elegante può essere sicuramente, ma in questo caso va bene: facile e leggibile.

Se la tua logica diventa più complicata, getta via quel codice e considera l'utilizzo di qualcosa di più per memorizzare diversi scenari disponibili (come suggerisce Zladeck).

Mi piace molto il primo suggerimento dato in questa risposta : facile da leggere, non soggetto a errori, gestibile

(Quasi) fuori tema:

Non scrivo molte risposte qui su StackOverflow. È davvero divertente che la risposta sopra accettata sia di gran lunga la risposta più apprezzata nella mia storia (non ho mai avuto più di 5-10 voti positivi prima di quanto penso) mentre in realtà non è quello che di solito penso sia il modo "giusto" per farlo.

Ma la semplicità è spesso "il modo giusto per farlo", molte persone sembrano pensarlo e io dovrei pensarlo più di me :)


1
certo @hessamhedieh, va bene solo per un piccolo numero di scenari disponibili. come ho detto, se le cose si complicano meglio cercare qualcos'altro
Gian Paolo

4
Ciò può essere ulteriormente semplificato impilando tutte le condizioni nell'inizializzatore valide separandole con ||, invece di modificarle validall'interno di blocchi di istruzioni separati. Non posso inserire un esempio nel commento ma puoi allineare verticalmente gli ||operatori lungo la sinistra per renderlo molto chiaro; le singole condizioni sono già tra parentesi quanto devono essere (per if) quindi non è necessario aggiungere alcun carattere alle espressioni oltre a ciò che è già presente.
Leushenko

1
@ Leushenko, penso che mescolando parentesi, && e || le condizioni sono abbastanza soggette a errori (qualcuno in una risposta diversa ha detto che c'era un errore tra parentesi nel codice in OP, forse è stato corretto). Un corretto allineamento può aiutare, certo. Ma qual è il vantaggio? più leggibile? più facile da mantenere? Non credo proprio. Solo la mia opinione, ovviamente. Certo, odio davvero avere molti if nel codice.
Gian Paolo

3
L'avrei racchiuso in un modo if($bValue1)che deve sempre essere vero, tecnicamente consentendo un piccolo miglioramento delle prestazioni (anche se qui stiamo parlando di importi trascurabili).
Martijn

2
FWIW: ci sono solo 2 scenari: i primi 2 sono lo stesso scenario e non dipendono dabValue4
Dancrumb

123

Mirerei alla semplicità e alla leggibilità.

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

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

Assicurati di sostituire i nomi degli scenari così come i nomi delle bandiere con qualcosa di descrittivo. Se ha senso per il tuo problema specifico, potresti prendere in considerazione questa alternativa:

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

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

Ciò che è importante qui non è la logica dei predicati. Descrive il tuo dominio ed esprime chiaramente il tuo intento. La chiave qui è dare a tutti gli input e alle variabili intermedie un buon nome. Se non riesci a trovare buoni nomi di variabili, potrebbe essere un segno che stai descrivendo il problema nel modo sbagliato.


3
+1 Questo è quello che avrei fatto anch'io. Proprio come fa notare @RedFilter, e in contrasto con la risposta accettata, questa è auto-documentata. Assegnare agli scenari i propri nomi in un passaggio separato è molto più leggibile.
Andreas

106

Possiamo usare una mappa di Karnaugh e ridurre i tuoi scenari a un'equazione logica. Ho usato il risolutore di mappe Karnaugh online con circuito per 4 variabili.

inserisci qui la descrizione dell'immagine

Questo produce:

inserisci qui la descrizione dell'immagine

Passando A, B, C, Da bValue1, bValue2, bValue3, bValue4, questo non è altro che:

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

Quindi la tua ifdichiarazione diventa:

if(!(bValue1 && bValue2 && bValue3 || bValue1 && !bValue2 && !bValue3 && !bValue4))
{
    // There is some error
}
  • Le mappe di Karnaugh sono particolarmente utili quando hai molte variabili e molte condizioni da valutare true.
  • Dopo aver ridotto gli truescenari a un'equazione logica, l'aggiunta di commenti pertinenti che indicano gli truescenari è una buona pratica.

96
Sebbene tecnicamente corretto, questo codice richiede molti commenti per essere modificato da un altro sviluppatore pochi mesi dopo.
Zdeslav Vojkovic

22
@ZdeslavVojkovic: vorrei solo aggiungere un commento con l'equazione. //!(ABC + AB'C'D') (By K-Map logic). Sarebbe un buon momento per lo sviluppatore per imparare le K-Maps se non le conosce già.
PW

11
Sono d'accordo con questo, ma IMO il problema è che non si mappa chiaramente al dominio del problema, cioè come ogni condizione si associa a uno scenario specifico che rende difficile cambiare / estendere. Cosa succede quando ci sono Ee Fcondizioni e 4 nuovi scenari? Quanto tempo ci vuole per aggiornare ifcorrettamente questa dichiarazione? In che modo la revisione del codice verifica se va bene o no? Il problema non è con il lato tecnico ma con il lato "business".
Zdeslav Vojkovic

7
Penso che tu possa escludere A: ABC + AB'C'D' = A(BC + B'C'D')(questo può anche essere preso in considerazione anche A(B ^ C)'(C + D')se starei attento a chiamarlo "semplificazione").
Maciej Piechotka

28
@PW Quel commento sembra comprensibile quanto il codice, ed è quindi un po 'inutile. Un commento migliore spiegherebbe come sei arrivato a quell'equazione, cioè che l'affermazione dovrebbe attivarsi per TTTT, TTTF e TFFF. A quel punto potresti anche scrivere queste tre condizioni nel codice e non aver bisogno di alcuna spiegazione.
Bernhard Barker

58

La vera domanda qui è: cosa succede quando un altro sviluppatore (o anche autore) deve modificare questo codice pochi mesi dopo.

Suggerirei di modellarlo come flag di bit:

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 sono presenti molti più scenari o più flag, l'approccio alla tabella è più leggibile ed estensibile rispetto all'utilizzo dei flag. Il supporto di un nuovo scenario richiede solo un'altra riga nella tabella.

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
Non il più manutenibile ma decisamente semplifica la condizione if. Quindi lasciare alcuni commenti sulle operazioni bit per bit sarà una necessità assoluta qui imo.
Adam Zahran

6
IMO, la tabella è l'approccio migliore in quanto scala meglio con scenari e flag aggiuntivi.
Zdeslav Vojkovic

Mi piace la tua prima soluzione, di facile lettura e modificabile. Vorrei apportare 2 miglioramenti: 1: assegnare valori a scenarioX con un'indicazione esplicita dei valori booleani utilizzati, ad esempio SCENARIO_2 = true << 3 | true << 2 | true << 1 | false;2: evitare le variabili SCENARIO_X e quindi memorizzare tutti gli scenari disponibili in un file <std::set<int>. L'aggiunta di uno scenario sarà solo qualcosa come mySet.insert( true << 3 | false << 2 | true << 1 | false;forse un po 'eccessivo per soli 3 scenari, OP ha accettato la soluzione rapida, sporca e facile che ho suggerito nella mia risposta.
Gian Paolo

4
Se stai usando C ++ 14 o versioni successive, suggerirei invece di usare letterali binari per la prima soluzione: 0b1111, 0b1110 e 0b1000 sono molto più chiari. Probabilmente puoi anche semplificarlo un po 'usando la libreria standard ( std::find?).
Bernhard Barker

2
Trovo che i letterali binari qui sarebbero un requisito minimo per rendere pulito il primo codice. Nella sua forma attuale è completamente criptico. Gli identificatori descrittivi potrebbero aiutare, ma non ne sono nemmeno sicuro. In effetti, le operazioni sui bit per produrre il scenariovalore mi sembrano inutilmente soggette a errori.
Konrad Rudolph

27

La mia risposta precedente è già la risposta accettata, aggiungo qui qualcosa che penso sia leggibile, facile e in questo caso aperto a modifiche future:

Iniziando con la risposta di @ZdeslavVojkovic (che trovo abbastanza buona), mi è venuta questa:

#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;
}

Guardalo al lavoro qui

Bene, questa è la soluzione "elegante e manutenibile" (IMHO) a cui di solito cerco, ma in realtà, per il caso OP, la mia precedente risposta "gruppo di se" si adatta meglio ai requisiti dell'OP, anche se non è elegante né manutenibile.


Sai che puoi sempre modificare la tua risposta precedente e apportare miglioramenti.
Andreas

20

Vorrei anche presentare un altro approccio.

La mia idea è convertire i bool in un numero intero e quindi confrontarli utilizzando modelli variadici:

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
    }
}

Notare come questo sistema può supportare fino a 32 bool come input. sostituire il unsignedcon unsigned long long(o uint64_t) aumenta il supporto a 64 casi. Se non ti piace if (summary != 0b1111u && summary != 0b1110u && summary != 0b1000u), puoi anche usare un altro metodo di template variadico:

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
Adoro questo approccio, ad eccezione del nome della funzione principale: "from bool ... to what ?" - Perché non esplicitamente bitmap_from_bools, o bools_to_bitmap?
Konrad Rudolph

si @KonradRudolph, non potrei pensare a un nome migliore, tranne forse bools_to_unsigned. Bitmap è una buona parola chiave; modificato.
Stack Danny

Penso che tu voglia summary!= 0b1111u &&.... a != b || a != cè sempre vero seb != c
MooseBoys

17

Ecco una versione semplificata:

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

Nota, ovviamente, questa soluzione è più offuscata dell'originale, il suo significato potrebbe essere più difficile da capire.


Aggiornamento: MSalters nei commenti ha trovato un'espressione ancora più semplice:

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

1
Sì, ma difficile da capire. Ma grazie per il suggerimento.
Andrew Truckle

Ho confrontato la capacità dei compilatori di semplificare l'espressione con la tua semplificazione come riferimento: esploratore del compilatore . gcc non ha trovato la tua versione ottimale ma la sua soluzione è ancora buona. Clang e MSVC non sembrano eseguire alcuna semplificazione delle espressioni booleane.
Oliv

1
@AndrewTruckle: nota che se avevi bisogno di una versione più leggibile, per favore dillo. Hai detto "semplificato", ma accetti una versione ancora più prolissa di quella originale.
geza

1
simpleè davvero un termine vago. Molte persone lo interpretano in questo contesto come più semplice da capire per lo sviluppatore e non per il compilatore generare codice, quindi più verboso può effettivamente essere più semplice.
Zdeslav Vojkovic

1
@IsmaelMiguel: quando una formula logica è ottimizzata per il numero di termini, il significato originale di solito viene perso. Ma si può aggiungere un commento, quindi è chiaro cosa fa. Anche, per la risposta accettata, un commento non danneggerebbe.
geza

12

Considera l'idea di tradurre le tue tabelle il più direttamente possibile nel tuo programma. Guida il programma in base al tavolo, invece di imitarlo con la logica.

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;
}

adesso

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

questo direttamente possibile codifica la tua tabella di verità nel compilatore.

Esempio dal vivo .

Puoi anche usare std::any_ofdirettamente:

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;
}) {
}

il compilatore può incorporare il codice ed eliminare qualsiasi iterazione e costruire la propria logica per te. Nel frattempo, il tuo codice riflette esattamente come hai concepito il problema.


La prima versione è così facile da leggere e così manutenibile, mi piace molto. Il secondo è più difficile da leggere, almeno per me, e richiede un livello di abilità c ++ forse superiore alla media, sicuramente superiore al mio. Non è qualcosa che tutti possono scrivere. Ho appena imparato qualcosa di nuovo, grazie
Gian Paolo

11

Fornisco solo la mia risposta qui come nei commenti che qualcuno ha suggerito per mostrare la mia soluzione. Voglio ringraziare tutti per le loro intuizioni.

Alla fine ho deciso di aggiungere tre nuovi booleanmetodi di "scenario" :

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));
}

Quindi sono stato in grado di applicare quelle della mia routine di convalida in questo modo:

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

Nella mia applicazione live i 4 valori bool vengono effettivamente estratti da un file DWORD cui sono codificati 4 valori.

Grazie ancora a tutti.


1
Grazie per aver condiviso la soluzione. :) In realtà è meglio del complesso se condizioni infernali. Forse puoi ancora nominare INCLUDE_ITEM1ecc. In un modo migliore e stai bene. :)
Hardik Modha

1
@HardikModha Beh, tecnicamente sono "articoli per studenti" e la bandiera sta per indicare se devono essere "inclusi". Quindi penso che il nome, anche se suona generico, sia effettivamente significativo in questo contesto. :)
Andrew Truckle

11

Non vedo alcuna risposta che dica di nominare gli scenari, anche se la soluzione dell'OP fa esattamente questo.

Per me è meglio incapsulare il commento di ciò che è ogni scenario in un nome di variabile o in un nome di funzione. È più probabile che tu ignori un commento piuttosto che un nome e, se la tua logica cambia in futuro, è più probabile che tu cambi un nome piuttosto che un commento. Non puoi effettuare il refactoring di un commento.

Se prevedi di riutilizzare questi scenari al di fuori della tua funzione (o potresti volerlo), crea una funzione che dica ciò che valuta ( constexpr/ noexceptopzionale ma consigliato):

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; }

Se possibile, crea questi metodi di classe (come nella soluzione di OP). Puoi utilizzare le variabili all'interno della tua funzione se non pensi di riutilizzare la logica:

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;

Il compilatore molto probabilmente risolverà che se bValue1 è falso, tutti gli scenari sono falsi. Non preoccuparti di renderlo veloce, solo corretto e leggibile. Se si profila il codice e si scopre che si tratta di un collo di bottiglia perché il compilatore ha generato un codice non ottimale a -O2 o superiore, provare a riscriverlo.


Mi piace leggermente di più rispetto alla soluzione (già carina) di Gian Paolo: evita il flusso di controllo e l'uso di una variabile che viene sovrascritta - stile più funzionale.
Dirk Herrmann

9

Modo 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;
}

Questo approccio è scalabile come se il numero di condizioni valide aumentasse, se ne aggiungessero facilmente altre all'elenco degli scenari.


Sono abbastanza sicuro che sia sbagliato, però. Si presuppone che il compilatore utilizzi solo una singola rappresentazione binaria per true. Un compilatore che utilizza "qualsiasi cosa diversa da zero è vera" fa sì che questo codice fallisca. Nota che truedeve essere convertito in 1, semplicemente non ha bisogno di essere memorizzato come tale.
MSalters

@MSalters, tnx, capisco il tuo punto e ne sono consapevole, tipo 2 is not equal to true but evaluates to true, il mio codice non forza int 1 = truee funziona fintanto che tutti i true vengono convertiti allo stesso valore int, quindi ecco la mia domanda: perché il compilatore dovrebbe agire in modo casuale durante la conversione fedele al sottostante int, puoi approfondire di più?
hessam hedieh

L'esecuzione di un memcmpper testare le condizioni booleane non è il modo C ++, e dubito piuttosto che sia nemmeno un modo C consolidato.
Konrad Rudolph

@ hessamhedieh: il problema nella tua logica è "convertire true in int sottostante". Cioè non come funzionano i compilatori,
MSalters

Il tuo codice aumenta la complessità da O (1) a O (n). Non è un modo per andare in nessuna lingua - lascia da parte C / C ++.
mabel

9

È facile notare che i primi due scenari sono simili: condividono la maggior parte delle condizioni. Se vuoi selezionare in quale scenario ti trovi in ​​questo momento, potresti scriverlo così (è una soluzione modificata di @ 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

Andando oltre, puoi notare, che il primo booleano deve essere sempre vero, che è una condizione di ingresso, quindi puoi finire con:

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
}

Ancora di più, ora puoi vedere chiaramente che bValue2 e bValue3 sono in qualche modo collegati: potresti estrarre il loro stato su alcune funzioni o variabili esterne con un nome più appropriato (questo non è sempre facile o appropriato però):

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
}

Farlo in questo modo presenta alcuni vantaggi e svantaggi:

  • le condizioni sono più piccole, quindi è più facile ragionarci su,
  • è più facile rinominare per rendere queste condizioni più comprensibili,
  • ma richiedono di comprenderne l'ambito
  • inoltre è più rigido

Se prevedi che ci saranno cambiamenti alla logica di cui sopra, dovresti usare un approccio più diretto come presentato da @ gian-paolo .

Altrimenti, se queste condizioni sono ben stabilite e sono una sorta di "regole solide" che non cambieranno mai, considera il mio ultimo frammento di codice.


7

Come suggerito da mch, potresti fare:

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

dove la prima riga copre i primi due casi buoni e la seconda riga copre l'ultimo.

Live Demo, dove ho giocato e passa i tuoi casi.


7

Una leggera variazione sulla bella risposta di @ GianPaolo, che alcuni potrebbero trovare più facile da leggere:

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

Ogni risposta è eccessivamente complessa e difficile da leggere. La migliore soluzione a questo è una switch()dichiarazione. È leggibile e semplifica l'aggiunta / modifica di casi aggiuntivi. Anche i compilatori sono bravi a ottimizzare le switch()dichiarazioni.

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;
}

Ovviamente puoi usare costanti e OR insieme nelle caseistruzioni per una leggibilità ancora maggiore.


Essendo un vecchio programmatore C, definirei una macro "PackBools" e la userei sia per "switch (PackBools (a, b, c, d))" e per i casi, ad esempio direttamente "case PackBools (true , true ...) "o definirli come costanti locali.eg" const unsigned int scenario1 = PackBools (true, true ...); "
Simon F

6

Vorrei anche utilizzare le variabili di scelta rapida per chiarezza. Come notato in precedenza, lo scenario 1 è uguale allo scenario 2, perché il valore di bValue4 non influenza la verità di questi due scenari.

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

allora la tua espressione diventa:

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

Dare nomi significativi alle variabili MAJORTRUE e MAJORFALSE (così come effettivamente a bValue * vars) aiuterebbe molto con la leggibilità e la manutenzione.


6

Concentrati sulla leggibilità del problema, non sull'istruzione "if" specifica.

Anche se questo produrrà più righe di codice, e alcuni potrebbero considerarlo eccessivo o non necessario. Suggerirei che astrarre i tuoi scenari dagli specifici valori booleani sia il modo migliore per mantenere la leggibilità.

Suddividendo le cose in classi (sentiti libero di usare solo le funzioni o qualsiasi altro strumento tu preferisca) con nomi comprensibili, possiamo mostrare molto più facilmente i significati dietro ogni scenario. Ancora più importante, in un sistema con molte parti mobili, è più facile mantenere e unirsi ai sistemi esistenti (di nuovo, nonostante la quantità di codice extra è coinvolta).

#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
Ad un certo punto, la verbosità inizia a danneggiare la leggibilità. Penso che questo vada troppo oltre.
JollyJoker

2
@ JollyJoker In realtà sono d'accordo in questa situazione specifica - tuttavia, la mia sensazione istintiva dal modo in cui OP ha chiamato tutto in modo estremamente generico, è che il loro codice "reale" è probabilmente molto più complesso dell'esempio che hanno fornito. In realtà, volevo solo mettere questa alternativa là fuori, poiché è così che la strutturerei per qualcosa di molto più complesso / coinvolgente. Ma hai ragione: per l'esempio specifico di OP, è eccessivamente prolisso e peggiora le cose.

5

Dipende da cosa rappresentano.

Ad esempio, se 1 è una chiave e 2 e 3 sono due persone che devono essere d'accordo (a meno che non siano d'accordo sul fatto che NOTabbiano bisogno di una terza persona - 4 - per confermare) il più leggibile potrebbe essere:

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

a richiesta popolare:

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

2
Potresti avere ragione, ma usare i numeri per illustrare il tuo punto sminuisce la tua risposta. Prova a usare nomi descrittivi.
jxh

1
@jxh Questi sono i numeri utilizzati da OP. Ho appena rimosso il file bValue.
ispiro

@jxh spero che sia meglio ora.
ispiro

4

L'esecuzione di operazioni bit per bit sembra molto chiara e comprensibile.

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

1
Il confronto bit per bit mi sembra leggibile. La composizione, d'altra parte, sembra artificiale.
xtofl

3

Indico a, b, c, d per chiarezza e A, B, C, D per i complementi

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

Equazione

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

Usa tutte le equazioni che ti si addice.


3
If (!bValue1 || (bValue2 != bValue3) || (!bValue4 && bValue2))
{
// you have a problem
}
  • b1 deve sempre essere vero
  • b2 deve sempre essere uguale a b3
  • e b4 non può essere falso se b2 (e b3) sono veri

semplice


3

Solo una preferenza personale rispetto alla risposta accettata, ma vorrei scrivere:

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

Innanzitutto, supponendo che sia possibile modificare solo il controllo dello scenario, mi concentrerei sulla leggibilità e racchiuderei il controllo in una funzione in modo che tu possa semplicemente chiamare if(ScenarioA()).


Ora, supponendo che tu voglia / hai bisogno di ottimizzarlo, consiglierei di convertire i Booleani strettamente collegati in numeri interi costanti e di utilizzare operatori bit su di essi

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
}

Ciò rende l'espressione degli scenari facile come elencare ciò che ne fa parte, consente di utilizzare un'istruzione switch per passare alla condizione giusta e confondere gli altri sviluppatori che non l'hanno mai visto prima. (C # RegexOptions usa questo modello per impostare i flag, non so se esiste un esempio di libreria c ++)


In realtà non sto usando quattro valori bool, ma un DWORD con quattro BOOL incorporati. È troppo tardi per cambiarlo adesso. Ma grazie per il tuo suggerimento.
Andrew Truckle

2

I messaggi nidificati ifpotrebbero essere più facili da leggere per alcune persone. Ecco la mia versione

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;
}

Personalmente, di solito eviterei di annidare le istruzioni if, se possibile. Sebbene questo caso sia piacevole e leggibile, una volta aggiunte nuove possibilità, l'annidamento può diventare molto difficile da leggere. Ma se gli scenari non cambiano mai, è sicuramente una soluzione piacevole e leggibile.
Dnomyar96

@ Dnomyar96 sono d'accordo. Personalmente evito anche i se annidati. A volte se la logica è complicata, è più facile per me comprenderla scomponendola in pezzi. Ad esempio, una volta inserito il bValue1blocco, puoi trattare tutto ciò che contiene come una nuova pagina fresca nel tuo processo mentale. Scommetto che il modo di affrontare il problema può essere molto personale o addirittura culturale.
sardok

1

Diverse risposte corrette sono state date a questa domanda, ma avrei una visione diversa: se il codice sembra troppo complicato, qualcosa non va bene . Il codice sarà difficile da eseguire il debug e sarà più probabile che sia "one-use-only".

Nella vita reale, quando troviamo una situazione come questa:

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

Quando quattro stati sono collegati da un modello così preciso, abbiamo a che fare con la configurazione di qualche "entità" nel nostro modello .

Una metafora estrema è come descriveremmo un "essere umano" in un modello, se non fossimo consapevoli della loro esistenza come entità unitarie con componenti connesse a specifici gradi di libertà: dovremmo descrivere stati indipendenti di "torso", "braccia", "gambe" e "testa" che renderebbero complicato dare un senso al sistema descritto. Un risultato immediato sarebbe espressioni booleane innaturalmente complicate.

Ovviamente, il modo per ridurre la complessità è l'astrazione e uno strumento di scelta in c ++ è il paradigma dell'oggetto .

Quindi la domanda è: perché esiste un tale schema? Cos'è e cosa rappresenta?

Poiché non conosciamo la risposta, possiamo ripiegare su un'astrazione matematica: l' array : abbiamo tre scenari, ognuno dei quali ora è un array.

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

A quel punto hai la tua configurazione iniziale. come un array. Per esempiostd::array ha un operatore di uguaglianza:

A quel punto la tua sintassi diventa:

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

}

Proprio come la risposta di Gian Paolo, è breve, chiara e facilmente verificabile / debuggabile. In questo caso, abbiamo delegato i dettagli delle espressioni booleane al compilatore.


1

Non dovrai preoccuparti di combinazioni non valide di flag booleani se ti sbarazzi dei flag booleani.

I valori accettabili sono:

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

Hai chiaramente tre stati (scenari). Sarebbe meglio modellarlo e derivare le proprietà booleane da quegli stati, non il contrario.

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;
    }
}

Questo è sicuramente più codice che nella risposta di Gian Paolo , ma a seconda della tua situazione, questo potrebbe essere molto più gestibile:

  • Esiste un insieme centrale di funzioni da modificare se vengono aggiunte proprietà o scenari booleani aggiuntivi.
    • L'aggiunta di proprietà richiede l'aggiunta di una sola funzione.
    • Se si aggiunge uno scenario, l'abilitazione degli avvisi del compilatore sui enumcasi non gestiti nelle switchistruzioni catturerà i generatori di proprietà che non gestiscono quello scenario.
  • Se è necessario modificare dinamicamente le proprietà booleane, non è necessario convalidare nuovamente le loro combinazioni ovunque. Invece di attivare o disattivare i singoli flag booleani (il che potrebbe comportare combinazioni di flag non valide), avresti invece una macchina a stati che passa da uno scenario a un altro.

Questo approccio ha anche il vantaggio collaterale di essere molto efficiente.


0

I miei 2 centesimi: dichiara una somma variabile (intero) in modo che

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

Controlla la somma rispetto alle condizioni che desideri e il gioco è fatto. In questo modo puoi aggiungere facilmente più condizioni in futuro, mantenendolo abbastanza semplice da leggere.


0

La risposta accettata va bene quando hai solo 3 casi e dove la logica per ciascuno è semplice.

Ma se la logica per ogni caso fosse più complicata, o ci fossero molti più casi, un'opzione di gran lunga migliore sarebbe quella di utilizzare il modello di progettazione della catena di responsabilità .

Si crea un BaseValidatorche contiene un riferimento a BaseValidatore un metodo a validatee un metodo per chiamare la convalida sul validatore di riferimento.

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;
    }
}

Quindi crei una serie di sottoclassi che ereditano da BaseValidator, sovrascrivendo il validatemetodo con la logica necessaria per ogni validatore.

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);
    }
}

Quindi usarlo è semplice, istanzia ciascuno dei tuoi validatori e imposta ciascuno di essi come radice degli altri:

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

In sostanza, ogni caso di convalida ha la propria classe che è responsabile di (a) determinare se la convalida corrisponde a quella caso e (b) inviare la convalida a qualcun altro nella catena se non lo è.

Tieni presente che non ho familiarità con il C ++. Ho provato ad abbinare la sintassi da alcuni esempi che ho trovato online, ma se questo non funziona, trattalo più come uno pseudocodice. Di seguito ho anche un esempio completo di Python funzionante che può essere utilizzato come base se si preferisce.

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

Di nuovo, potresti trovare questo eccessivo per il tuo esempio specifico, ma crea un codice molto più pulito se ti ritrovi con una serie di casi molto più complicata che devono essere soddisfatte.


-2

Un approccio semplice è trovare la risposta che ritieni accettabile.

Sì = (boolean1 && boolean2 && boolean3 && boolean4) + + ...

Ora, se possibile, semplifica l'equazione usando l'algebra booleana.

come in questo caso, accettabile1 e 2 si combinano con (boolean1 && boolean2 && boolean3).

Quindi la risposta finale è:

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

-3

usa campo bit :

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 :

È un vero peccato per i CPP. Ma UB non è la mia preoccupazione, controllalo su http://coliru.stacked-crooked.com/a/2b556abfc28574a1 .


2
Ciò causa UB a causa dell'accesso a un campo unione inattivo.
HolyBlackCat

Formalmente è UB in C ++, non è possibile impostare un membro di union e leggere da un altro. Tecnicamente potrebbe essere meglio implementare getter \ setters basati su modelli per bit di valore integrale.
Swift - Friday Pie

Penso che il comportamento passerebbe a Definito dall'implementazione se si convertisse l'indirizzo del sindacato in un unsigned char*, anche se penso che semplicemente usare qualcosa di simile ((((flag4 <<1) | flag3) << 1) | flag2) << 1) | flag1sarebbe probabilmente più efficiente.
supercat
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.