Cómo mejorar la lógica para verificar si 4 valores booleanos coinciden con algunos casos

118

Tengo cuatro boolvalores:

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

Los valores aceptables son:

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

Entonces, por ejemplo, este escenario no es aceptable:

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

De momento se me ha ocurrido esta ifdeclaración para detectar malos escenarios:

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

¿Se puede mejorar / simplificar esa lógica de declaración?

Andrew Truckle
fuente
8
Usaría una tabla en lugar de una ifdeclaración compleja . Además, como se trata de indicadores booleanos, puede modelar cada escenario como una constante y compararlo.
Zdeslav Vojkovic
3
if (!((bValue1 && bValue2 && bValue3) || (bValue1 && !bValue2 && !bValue3 && !bValue4)))
marzo
14
¿Cuáles son los escenarios en realidad? A menudo, las cosas se vuelven mucho más simples si solo le das nombres propios a las cosas, por ejemplobool scenario1 = bValue1 && bValue2 && bValue3 && bValue4;
idclev 463035818
6
Usando nombres significativos, puede extraer cada condición compleja en un método y llamar a ese método en condición if. Sería mucho más legible y fácil de mantener. Por ejemplo, observe el ejemplo proporcionado en el enlace. refactoring.guru/decompose-conditional
Hardik Modha

Respuestas:

195

Mi objetivo es la legibilidad: solo tiene 3 escenarios, trátelos con 3 if 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 leer y depurar, en mi humilde opinión. Además, puede asignar una variable whichScenariomientras continúa con el if.

Con solo 3 escenarios, no iría con algo como "si los primeros 3 valores son verdaderos, puedo evitar comprobar el cuarto valor": hará que su código sea más difícil de leer y mantener.

No es una solución elegante tal vez seguramente, pero en este caso está bien: fácil y legible.

Si su lógica se vuelve más complicada, deseche ese código y considere usar algo más para almacenar diferentes escenarios disponibles (como sugiere Zladeck).

Realmente me encanta la primera sugerencia dada en esta respuesta : fácil de leer, no propenso a errores, mantenible

(Casi) fuera del tema:

No escribo muchas respuestas aquí en StackOverflow. Es realmente gracioso que la respuesta aceptada anteriormente sea, con mucho, la respuesta más apreciada en mi historia (creo que nunca tuve más de 5-10 votos a favor) mientras que en realidad no es lo que normalmente creo que es la forma "correcta" de hacerlo.

Pero la simplicidad es a menudo "la forma correcta de hacerlo", mucha gente parece pensar esto y yo debería pensarlo más que yo :)

Gian Paolo
fuente
1
seguro @hessamhedieh, está bien solo para una pequeña cantidad de escenarios disponibles. como dije, si las cosas se complican, mejor busque otra cosa
Gian Paolo
4
Esto se puede simplificar aún más apilando todas las condiciones en el inicializador validy separándolas con ||, en lugar de mutar validdentro de bloques de instrucciones separados. No puedo poner un ejemplo en el comentario, pero puede alinear verticalmente los ||operadores a la izquierda para dejar esto muy claro; las condiciones individuales ya están entre paréntesis tanto como sea necesario (para if) por lo que no es necesario agregar ningún carácter a las expresiones más allá de lo que ya está allí.
Leushenko
1
@Leushenko, creo que mezclar paréntesis, && y || Las condiciones son bastante propensas a errores (alguien en una respuesta diferente dijo que había un error entre paréntesis en el código en OP, tal vez se haya corregido). La alineación adecuada puede ayudar, seguro. ¿Pero cuál es la ventaja? más legible? más fácil de mantener? No lo creo. Sólo mi opinión, por supuesto. Y asegúrese, realmente odio tener muchos ifs en el código.
Gian Paolo
3
Lo hubiera envuelto en un if($bValue1)ya que siempre tiene que ser cierto, lo que técnicamente permite una pequeña mejora en el rendimiento (aunque aquí estamos hablando de cantidades insignificantes).
Martijn
2
FWIW: solo hay 2 escenarios: los 2 primeros son el mismo escenario y no dependen debValue4
Dancrumb
123

Mi objetivo es la simplicidad y la legibilidad.

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

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

Asegúrese de reemplazar los nombres de los escenarios, así como los nombres de las banderas, con algo descriptivo. Si tiene sentido para su problema específico, podría considerar esta alternativa:

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

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

Lo importante aquí no es la lógica de predicados. Describe tu dominio y expresa claramente tu intención. La clave aquí es dar un buen nombre a todas las entradas y variables intermedias. Si no puede encontrar buenos nombres de variables, puede ser una señal de que está describiendo el problema de manera incorrecta.

Anders
fuente
3
+1 Esto es lo que yo también hubiera hecho. Como señala @RedFilter, y en contraste con la respuesta aceptada, esto es autodocumentado. Dar a los escenarios sus propios nombres en un paso separado es mucho más legible.
Andreas
106

Podemos usar un mapa de Karnaugh y reducir sus escenarios a una ecuación lógica. He utilizado el solucionador de mapas de Karnaugh en línea con circuito para 4 variables.

ingrese la descripción de la imagen aquí

Esto produce:

ingrese la descripción de la imagen aquí

Cambiando A, B, C, Da bValue1, bValue2, bValue3, bValue4, esto no es más que:

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

Entonces tu ifdeclaración se convierte en:

if(!(bValue1 && bValue2 && bValue3 || bValue1 && !bValue2 && !bValue3 && !bValue4))
{
    // There is some error
}
  • Los mapas de Karnaugh son particularmente útiles cuando tiene muchas variables y muchas condiciones que deben evaluar true .
  • Después de reducir los trueescenarios a una ecuación lógica, agregar comentarios relevantes que indiquen los trueescenarios es una buena práctica.
PW
fuente
96
Aunque técnicamente es correcto, este código requiere muchos comentarios para que otro desarrollador lo pueda editar unos meses después.
Zdeslav Vojkovic
22
@ZdeslavVojkovic: Solo agregaría un comentario con la ecuación. //!(ABC + AB'C'D') (By K-Map logic). Ese sería un buen momento para que el desarrollador aprenda K-Maps si aún no los conoce.
PV
11
Estoy de acuerdo con eso, pero en mi opinión, el problema es que no se asigna claramente al dominio del problema, es decir, cómo cada condición se asigna a un escenario específico, lo que hace que sea difícil de cambiar / extender. ¿Qué ocurre cuando hay Ey Fcondiciones y 4 nuevos escenarios? ¿Cuánto tiempo se tarda en actualizar esta ifdeclaración correctamente? ¿Cómo comprueba la revisión de código si está bien o no? El problema no está en el aspecto técnico, sino en el aspecto "comercial".
Zdeslav Vojkovic
7
Creo que puede factorizar A: ABC + AB'C'D' = A(BC + B'C'D')(esto se puede incluso factorizar, A(B ^ C)'(C + D')aunque tendría cuidado al llamar a esto 'simplificación').
Maciej Piechotka
28
@PW Ese comentario parece tan comprensible como el código y, por lo tanto, no tiene sentido. Un mejor comentario explicaría cómo se le ocurrió esa ecuación, es decir, que la declaración debería activarse para TTTT, TTTF y TFFF. En ese punto, también podría escribir esas tres condiciones en el código y no necesitar una explicación en absoluto.
Bernhard Barker
58

La verdadera pregunta aquí es: ¿qué sucede cuando otro desarrollador (o incluso autor) debe cambiar este código unos meses después?

Sugeriría modelar esto como banderas 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");

Si hay muchos más escenarios o más indicadores, un enfoque de tabla es más legible y extensible que usar indicadores. Apoyar un nuevo escenario requiere simplemente otra fila en la tabla.

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");
}
Zdeslav Vojkovic
fuente
4
No es el más fácil de mantener, pero definitivamente simplifica la condición if. Entonces, dejar algunos comentarios sobre las operaciones bit a bit será una necesidad absoluta aquí, en mi opinión.
Adam Zahran
6
En mi opinión, la tabla es el mejor enfoque, ya que escala mejor con escenarios y banderas adicionales.
Zdeslav Vojkovic
Me gusta su primera solución, fácil de leer y abierta a modificaciones. Haría 2 mejoras: 1: asignar valores al escenarioX con una indicación explícita de los valores booleanos utilizados, por ejemplo, SCENARIO_2 = true << 3 | true << 2 | true << 1 | false;2: evitar las variables SCENARIO_X y luego almacenar todos los escenarios disponibles en a <std::set<int>. Agregar un escenario va a ser algo como mySet.insert( true << 3 | false << 2 | true << 1 | false;quizás un poco exagerado para solo 3 escenarios, OP aceptó la solución rápida, sucia y fácil que sugerí en mi respuesta.
Gian Paolo
4
Si está utilizando C ++ 14 o superior, le sugiero que utilice literales binarios para la primera solución: 0b1111, 0b1110 y 0b1000 es mucho más claro. Probablemente también pueda simplificar esto un poco usando la biblioteca estándar ( std::find?).
Bernhard Barker
2
Encuentro que los literales binarios aquí serían un requisito mínimo para limpiar el primer código. En su forma actual, es completamente críptico. Los identificadores descriptivos pueden ayudar, pero ni siquiera estoy seguro de eso. De hecho, las operaciones de bits para producir el scenariovalor me parecen innecesariamente propensas a errores.
Konrad Rudolph
27

Mi respuesta anterior ya es la respuesta aceptada, agrego algo aquí que creo que es legible, fácil y en este caso abierto a modificaciones futuras:

Comenzando con la respuesta de @ZdeslavVojkovic (que me parece bastante buena), se me ocurrió esto:

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

Véalo en el trabajo aquí

Bueno, esa es la solución "elegante y mantenible" (en mi humilde opinión) a la que suelo aspirar, pero en realidad, para el caso OP, mi anterior respuesta "montón de ifs" se ajusta mejor a los requisitos OP, incluso si no es elegante ni fácil de mantener.

Gian Paolo
fuente
Sabes que siempre puedes editar tu respuesta anterior y realizar mejoras.
Andreas
20

También me gustaría presentar otro enfoque.

Mi idea es convertir los bools en un número entero y luego comparar usando plantillas variadic:

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 cómo este sistema puede admitir hasta 32 bools como entrada. reemplazar el unsignedcon unsigned long long(o uint64_t) aumenta el soporte a 64 casos. Si no le gusta if (summary != 0b1111u && summary != 0b1110u && summary != 0b1000u), también puede usar otro método de plantilla variadic:

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
    }
}
Pila de Danny
fuente
2
Me encanta este enfoque, excepto por el nombre de la función principal: "de bool ... ¿ a qué ?" - ¿Por qué no explícitamente bitmap_from_bools, o bools_to_bitmap?
Konrad Rudolph
sí @KonradRudolph, no podría pensar en un nombre mejor, excepto tal vez bools_to_unsigned. Mapa de bits es una buena palabra clave; editado.
Stack Danny
Creo que quieres summary!= 0b1111u &&.... a != b || a != csiempre es cierto sib != c
MooseBoys
17

Aquí tienes una versión simplificada:

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

Tenga en cuenta, por supuesto, que esta solución está más confusa que la original, su significado puede ser más difícil de entender.


Actualización: MSalters en los comentarios encontró una expresión aún más simple:

if (bValue1&&(bValue2==bValue3)&&(bValue2>=bValue4)) ...
geza
fuente
1
Sí, pero difícil de entender. Pero gracias por la sugerencia.
Andrew Truckle
Comparé la capacidad de los compiladores para simplificar la expresión con su simplificación como referencia: explorador de compiladores . gcc no encontró su versión óptima, pero su solución sigue siendo buena. Clang y MSVC no parecen realizar ninguna simplificación de expresión booleana.
Oliv
1
@AndrewTruckle: tenga en cuenta que si necesita una versión más legible, dígalo. Ha dicho "simplificado", pero acepta una versión aún más detallada que la original.
geza
1
simplees de hecho un término vago. Mucha gente lo entiende en este contexto como más simple de entender para el desarrollador y no para que el compilador genere código, por lo que más detallado puede ser más simple.
Zdeslav Vojkovic
1
@IsmaelMiguel: cuando una fórmula lógica se optimiza para varios términos, generalmente se pierde el significado original. Pero se puede poner un comentario al respecto, para que quede claro lo que hace. Incluso, para la respuesta aceptada, un comentario no estaría mal.
geza
12

Considere traducir sus tablas lo más directamente posible a su programa. Elimine el programa basado en la mesa, en lugar de imitarlo con 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;
}

ahora

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

esto directamente codifica su tabla de verdad en el compilador.

Ejemplo vivo .

También puede usar std::any_ofdirectamente:

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

el compilador puede incorporar el código, eliminar cualquier iteración y construir su propia lógica para usted. Mientras tanto, su código refleja exactamente cómo concibió el problema.

Yakk - Adam Nevraumont
fuente
La primera versión es tan fácil de leer y tan fácil de mantener, realmente me gusta. El segundo es más difícil de leer, al menos para mí, y requiere un nivel de habilidad de C ++ tal vez por encima del promedio, seguramente por encima del mío. No es algo que todo el mundo pueda escribir. Acabo de aprender algo nuevo, gracias
Gian Paolo
11

Solo proporciono mi respuesta aquí como en los comentarios que alguien sugirió para mostrar mi solución. Quiero agradecer a todos por sus ideas.

Al final, opté por agregar tres nuevos booleanmétodos de "escenario" :

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

Luego pude aplicar esas mi rutina de validación de esta manera:

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

En mi aplicación en vivo, los 4 valores bool se extraen de un DWORD que tiene 4 valores codificados.

Gracias de nuevo a todos.

Andrew Truckle
fuente
1
Gracias por compartir la solucion. :) En realidad, es mejor que el complejo si las condiciones del infierno. Tal vez aún puedas nombrar, INCLUDE_ITEM1etc. de una mejor manera y estás bien. :)
Hardik Modha
1
@HardikModha Bueno, técnicamente son "artículos para estudiantes" y la bandera es para indicar si se deben "incluir". Así que creo que el nombre, aunque suene genérico, es realmente significativo en este contexto. :)
Andrew Truckle
11

No veo ninguna respuesta que diga que nombre los escenarios, aunque la solución del OP hace exactamente eso.

Para mí, es mejor encapsular el comentario de lo que es cada escenario en un nombre de variable o de función. Es más probable que ignore un comentario que un nombre, y si su lógica cambia en el futuro, es más probable que cambie un nombre que un comentario. No puedes refactorizar un comentario.

Si planeas reutilizar estos escenarios fuera de tu función (o tal vez quieras), crea una función que diga lo que evalúa ( constexpr/ noexceptopcional pero 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; }

Haga estos métodos de clase si es posible (como en la solución de OP). Puede usar variables dentro de su función si no cree que reutilizará la 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;

Lo más probable es que el compilador resuelva que si bValue1 es falso, todos los escenarios son falsos. No se preocupe por hacerlo rápido, simplemente correcto y legible. Si perfila su código y encuentra que esto es un cuello de botella porque el compilador generó un código subóptimo en -O2 o superior, intente reescribirlo.

Erróneo
fuente
Me gusta esto un poco más que la solución (ya agradable) de Gian Paolo: evita el flujo de control y el uso de una variable que se sobrescribe, un estilo más funcional.
Dirk Herrmann
9

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

Este enfoque es escalable, ya que si aumentara el número de condiciones válidas, fácilmente se agregan más a la lista de escenarios.

hessam hedieh
fuente
Sin embargo, estoy bastante seguro de que esto está mal. Asume que el compilador usa solo una representación binaria para true. Un compilador que usa "cualquier cosa distinta de cero es verdadera" hace que este código falle. Tenga en cuenta que truedebe convertirse a 1, simplemente no necesita almacenarse como tal.
MSalters
@MSalters, tnx, entiendo su punto y soy consciente de eso, algo así como 2 is not equal to true but evaluates to true, mi código no fuerza int 1 = truey funciona siempre que todos los verdaderos se conviertan al mismo valor int, así que aquí está mi pregunta: ¿Por qué el compilador debería actuar al azar al convertir fiel al int subyacente, ¿podría dar más detalles?
hessam hedieh
Realizar una memcmpprueba de condiciones booleanas no es la forma de C ++, y dudo que sea una forma de C establecida.
Konrad Rudolph
@hessamhedieh: El problema en su lógica es "convertir verdadero en int subyacente". Eso no es como funcionan los compiladores
MSalters
Su código aumenta la complejidad de O (1) a O (n). No es una forma de hacerlo en ningún idioma, deje de lado C / C ++.
mabel
9

Es fácil notar que los dos primeros escenarios son similares: comparten la mayoría de las condiciones. Si desea seleccionar en qué escenario se encuentra en este momento, puede escribirlo así (es una solución de @ gian-paolo modificada ):

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

Yendo más allá, puede notar que el primer booleano debe ser siempre verdadero, que es una condición de entrada, por lo que puede terminar 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
}

Aún más, ahora puede ver claramente que bValue2 y bValue3 están algo conectados; podría extraer su estado a algunas funciones externas o variables con un nombre más apropiado (aunque esto no siempre es fácil o apropiado):

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
}

Hacerlo de esta manera tiene algunas ventajas y desventajas:

  • las condiciones son más pequeñas, por lo que es más fácil razonar sobre ellas,
  • es más fácil cambiar el nombre para que estas condiciones sean más comprensibles,
  • pero, necesitan entender el alcance,
  • además es más rígido

Si predice que habrá cambios en la lógica anterior, debe usar un enfoque más sencillo como lo presenta @ gian-paolo .

De lo contrario, si estas condiciones están bien establecidas y son una especie de "reglas sólidas" que nunca cambiarán, considere mi último fragmento de código.

Michał Łoś
fuente
7

Como sugirió mch, podría hacer:

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

donde la primera línea cubre los dos primeros casos buenos y la segunda línea cubre el último.

Live Demo, donde jugué y pasa tus casos.

gsamaras
fuente
7

Una ligera variación en la excelente respuesta de @ GianPaolo, que algunos pueden encontrar más fácil de leer:

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

Cada respuesta es demasiado compleja y difícil de leer. La mejor solución a esto es una switch()declaración. Es legible y simplifica la adición / modificación de casos adicionales. Los compiladores también son buenos para optimizar switch()declaraciones.

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

Por supuesto, puede usar constantes y OR juntas en las casedeclaraciones para una mayor legibilidad.

shogged
fuente
Siendo un antiguo programador en C, definiría una macro "PackBools" y la usaría tanto para el "switch (PackBools (a, b, c, d))" como para los casos, por ejemplo, directamente "case PackBools (true , verdadero ...) "o definirlos como constantes locales.eg" const unsigned int escenario1 = PackBools (verdadero, verdadero ...); "
Simon F
6

También usaría variables de atajo para mayor claridad. Como se señaló anteriormente, el escenario 1 es igual al escenario 2, porque el valor de bValue4 no influye en la verdad de esos dos escenarios.

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

entonces tu expresión se convierte en:

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

Dar nombres significativos a las variables MAJORTRUE y MAJORFALSE (así como en realidad a bValue * vars) ayudaría mucho con la legibilidad y el mantenimiento.

Gnudiff
fuente
6

Concéntrese en la legibilidad del problema, no en la declaración específica "si".

Si bien esto producirá más líneas de código, algunos pueden considerarlo excesivo o innecesario. Sugeriría que abstraer sus escenarios de los valores booleanos específicos es la mejor manera de mantener la legibilidad.

Al dividir las cosas en clases (siéntase libre de usar funciones, o cualquier otra herramienta que prefiera) con nombres comprensibles, podemos mostrar mucho más fácilmente los significados detrás de cada escenario. Más importante aún, en un sistema con muchas partes móviles, es más fácil mantener y unir sus sistemas existentes (nuevamente, a pesar de la cantidad de código adicional que se invoque).

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

fuente
5
En algún momento, la verbosidad comienza a dañar la legibilidad. Creo que esto va demasiado lejos.
JollyJoker
2
@JollyJoker Estoy de acuerdo en esta situación específica; sin embargo, mi intuición por la forma en que OP ha nombrado todo de manera extremadamente genérica es que su código "real" es probablemente mucho más complejo que el ejemplo que han dado. Realmente, solo quería presentar esta alternativa, ya que así es como la estructuraría para algo mucho más complejo / complicado. Pero tiene razón: para el ejemplo específico de OP, es demasiado detallado y empeora las cosas.
5

Depende de lo que representen.

Por ejemplo, si 1 es una clave y 2 y 3 son dos personas que deben estar de acuerdo (excepto si están de acuerdo en NOTque necesitan una tercera persona, 4 , para confirmar), la más legible podría ser:

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

por solicitud popular:

Key &&
    (
        (Alice && Bob)   
        || 
        ((!Alice && !Bob) && !Charlie)
    )
ispiro
fuente
2
Puede que tengas razón, pero usar números para ilustrar tu punto le resta valor a tu respuesta. Intente usar nombres descriptivos.
jxh
1
@jxh Esos son los números que OP utilizó. Acabo de quitar el bValue.
ispiro
@jxh Espero que esté mejor ahora.
ispiro
4

La operación bit a bit se ve muy limpia y comprensible.

int bitwise = (bValue4 << 3) | (bValue3 << 2) | (bValue2 << 1) | (bValue1);
if (bitwise == 0b1111 || bitwise == 0b0111 || bitwise == 0b0001)
{
    //satisfying condition
}
Derviş Kayımbaşıoğlu
fuente
1
La comparación bit a bit me parece legible. La composición, por otro lado, parece artificial.
xtofl
3

Estoy denotando a, b, c, d para mayor claridad y A, B, C, D para complementos.

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

Ecuación

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

Utilice cualquier ecuación que le convenga.

mabel
fuente
3
If (!bValue1 || (bValue2 != bValue3) || (!bValue4 && bValue2))
{
// you have a problem
}
  • b1 siempre debe ser cierto
  • b2 siempre debe ser igual a b3
  • y b4 no puede ser falso si b2 (y b3) son verdaderos

sencillo

Owen Meyer
fuente
3

Solo una preferencia personal sobre la respuesta aceptada, pero escribiría:

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);
François Gueguen
fuente
2

Primero, asumiendo que solo puede modificar la verificación del escenario, me centraría en la legibilidad y simplemente envolvería la verificación en una función para que pueda simplemente llamar if(ScenarioA()).


Ahora, asumiendo que realmente desea / necesita optimizar esto, recomendaría convertir los booleanos estrechamente vinculados en enteros constantes y usar operadores de bits en ellos

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
}

Esto hace que expresar los escenarios sea tan fácil como enumerar lo que es parte de ellos, le permite usar una declaración de cambio para saltar a la condición correcta y confundir a otros desarrolladores que no han visto esto antes. (C # RegexOptions usa este patrón para configurar banderas, no sé si hay un ejemplo de biblioteca de C ++)

Tezra
fuente
De hecho, no estoy usando cuatro valores bool sino un DWORD con cuatro BOOLS incrustados. Demasiado tarde para cambiarlo ahora. Pero gracias por tu sugerencia.
Andrew Truckle
2

Los mensajes de correo electrónico anidados ifpodrían ser más fáciles de leer para algunas personas. Aqui esta mi version

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;
}
sardok
fuente
Personalmente, normalmente evitaría anidar declaraciones if si es posible. Si bien este caso es agradable y legible, una vez que se agregan nuevas posibilidades, el anidamiento puede volverse muy difícil de leer. Pero si los escenarios nunca cambian, definitivamente es una solución agradable y legible.
Dnomyar96
@ Dnomyar96 estoy de acuerdo. Yo personalmente también evito los ifs anidados. A veces, si la lógica es complicada, es más fácil para mí entender la lógica dividiéndola en pedazos. Por ejemplo, una vez que ingresa al bValue1bloque, puede tratar todo lo que contiene como una nueva página nueva en su proceso mental. Apuesto a que la forma de abordar el problema puede ser muy personal o incluso cultural.
sardok
1

Se han dado varias respuestas correctas a esta pregunta, pero yo adoptaría un punto de vista diferente: si el código parece demasiado complicado, algo no está del todo bien . El código será difícil de depurar y es más probable que sea de "un solo uso".

En la vida real, cuando nos encontramos con una situación como esta:

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

Cuando cuatro estados están conectados por un patrón tan preciso, estamos tratando con la configuración de alguna "entidad" en nuestro modelo. .

Una metáfora extrema es cómo describiríamos a un "ser humano" en un modelo, si no fuéramos conscientes de su existencia como entidades unitarias con componentes conectados en grados específicos de libertad: tendríamos que describir estados independientes de "torsoes", "brazos", "piernas" y "cabeza", lo que complicaría la comprensión del sistema descrito. Un resultado inmediato serían expresiones booleanas anormalmente complicadas.

Obviamente, la forma de reducir la complejidad es la abstracción y una herramienta de elección en c ++ es el paradigma del objeto. .

Entonces la pregunta es: ¿por qué existe tal patrón? ¿Qué es esto y qué representa?

Como no sabemos la respuesta, podemos recurrir a una abstracción matemática: la matriz : tenemos tres escenarios, cada uno de los cuales ahora es una matriz.

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

En ese momento tienes tu configuración inicial. como una matriz. P.ejstd::array tiene un operador de igualdad:

En ese momento su sintaxis se convierte en:

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

}

Al igual que la respuesta de Gian Paolo, es breve, clara y fácilmente verificable / depurable. En este caso, hemos delegado los detalles de las expresiones booleanas al compilador.

fralau
fuente
1

No tendrá que preocuparse por combinaciones no válidas de indicadores booleanos si se deshace de los indicadores booleanos.

Los valores aceptables son:

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

Claramente tienes tres estados (escenarios). Sería mejor modelar eso y derivar las propiedades booleanas de esos estados, no al revés.

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

Este es definitivamente más código que en la respuesta de Gian Paolo , pero dependiendo de su situación, esto podría ser mucho más fácil de mantener:

  • Existe un conjunto central de funciones para modificar si se agregan propiedades o escenarios booleanos adicionales.
    • Agregar propiedades requiere agregar solo una función.
    • Si agrega un escenario, habilitar las advertencias del compilador sobre enumcasos switchno manejados en declaraciones detectará a los captadores de propiedades que no manejan ese escenario.
  • Si necesita modificar las propiedades booleanas de forma dinámica, no necesita volver a validar sus combinaciones en todas partes. En lugar de alternar indicadores booleanos individuales (lo que podría resultar en combinaciones inválidas de indicadores), tendría una máquina de estado que pasa de un escenario a otro.

Este enfoque también tiene el beneficio adicional de ser muy eficiente.

jamesdlin
fuente
0

Mis 2 centavos: declare una suma variable (entero) para que

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

Verifique la suma con las condiciones que desee y listo. De esta manera, puede agregar fácilmente más condiciones en el futuro, manteniéndolo bastante sencillo de leer.

SCdev
fuente
0

La respuesta aceptada está bien cuando solo tiene 3 casos y donde la lógica para cada uno es simple.

Pero si la lógica para cada caso fuera más complicada, o hay muchos más casos, una opción mucho mejor es utilizar la cadena de responsabilidad. patrón de diseño de .

Creas un BaseValidatorque contiene una referencia a un BaseValidatory un método validatey un método para llamar a la validación en el validador al que se hace referencia.

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

Luego, crea una serie de subclases que heredan de BaseValidator, anulando el validatemétodo con la lógica necesaria 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);
    }
}

Luego, usarlo es simple, cree una instancia de cada uno de sus validadores y configure cada uno de ellos para que sea la raíz de los demás:

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

En esencia, cada caso de validación tiene su propia clase que es responsable de (a) determinar si la validación coincide con esa caso, y (b) enviar la validación a otra persona en la cadena si no lo es.

Tenga en cuenta que no estoy familiarizado con C ++. Intenté hacer coincidir la sintaxis de algunos ejemplos que encontré en línea, pero si esto no funciona, trátelo más como un pseudocódigo. También tengo un ejemplo completo de Python funcional a continuación que se puede usar como base si se prefiere.

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

Nuevamente, puede encontrar esto excesivo para su ejemplo específico, pero crea un código mucho más limpio si termina con un conjunto de casos mucho más complicado que deben cumplirse.

Jim Cullen
fuente
-2

Un enfoque simple es encontrar la respuesta que crea aceptable.

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

Ahora, si es posible, simplifique la ecuación usando álgebra booleana.

como en este caso, aceptable1 y 2 se combinan para (boolean1 && boolean2 && boolean3).

Por lo tanto, la respuesta final es:

(boolean1 && boolean2 && boolean3) || 
((boolean1 && !boolean2 && !boolean3 && !boolean4)
Rupesh
fuente
-3

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

PD :

Es una gran lástima para los CPPers. Pero, UB no es mi preocupación, compruébalo en http://coliru.stacked-crooked.com/a/2b556abfc28574a1 .

hedzr
fuente
2
Esto provoca UB por acceder a un campo de unión inactivo.
HolyBlackCat
Formalmente es UB en C ++, no puede configurar un miembro del sindicato y leer de otro. Técnicamente, podría ser mejor implementar getters \ setters con plantilla para bits de valor integral.
Swift - Friday Pie
Creo que el comportamiento cambiaría a Definido por implementación si se convirtiera la dirección del sindicato en un unsigned char*, aunque creo que simplemente usar algo como ((((flag4 <<1) | flag3) << 1) | flag2) << 1) | flag1probablemente sería más eficiente.
supercat