ChatGPT解决这个技术问题 Extra ChatGPT

如何改进逻辑以检查 4 个布尔值是否匹配某些情况

我有四个 bool 值:

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

可接受的值为:

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

因此,例如,这种情况是不可接受的:

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

目前,我提出了这个 if 语句来检测不良情况:

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

可以改进/简化该语句逻辑吗?

我会使用表格而不是复杂的 if 语句。此外,由于这些是布尔标志,您可以将每个场景建模为常量并对其进行检查。
if (!((bValue1 && bValue2 && bValue3) || (bValue1 && !bValue2 && !bValue3 && !bValue4)))
实际上有哪些场景?如果您只给东西起正确的名字,事情通常会变得简单得多,例如 bool scenario1 = bValue1 && bValue2 && bValue3 && bValue4;
使用有意义的名称,您可以将每个复杂条件提取到一个方法中,并在 if 条件中调用该方法。它将更具可读性和可维护性。例如,查看链接中提供的示例。refactoring.guru/decompose-conditional

G
Gian Paolo

我的目标是可读性:你只有 3 个场景,用 3 个单独的 if 处理它们:

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

恕我直言,易于阅读和调试。此外,您可以在处理 if 时分配一个变量 whichScenario

只有 3 个场景,我不会选择“如果前 3 个值是真的,我可以避免检查第四个值”这样的东西:它会让你的代码更难阅读和维护。

可能肯定不是一个优雅的解决方案,但在这种情况下是可以的:简单易读。

如果您的逻辑变得更复杂,请丢弃该代码并考虑使用更多内容来存储不同的可用场景(正如 Zladeck 所建议的那样)。

我真的很喜欢 this answer 中给出的第一个建议:易于阅读、不易出错、可维护

(几乎)题外话:

我不会在 StackOverflow 上写很多答案。非常有趣的是,上述接受的答案是迄今为止我历史上最受赞赏的答案(在我认为之前从未有超过 5-10 次投票),而实际上并不是我通常认为的“正确”方法。

但简单往往是“正确的做法”,很多人似乎都这么认为,我应该比我想得更多:)


当然@hessamhedieh,仅适用于少数可用场景。正如我所说,如果事情变得更复杂,最好寻找其他东西
这可以通过将所有条件堆叠到 valid 的初始化程序中并用 || 分隔它们来进一步简化,而不是在单独的语句块中改变 valid。我无法在评论中举例,但您可以将 || 运算符沿左侧垂直对齐以使这一点非常清楚;各个条件已根据需要用括号括起来(对于 if),因此您无需在表达式中添加任何已存在的字符。
@Leushenko,我认为混合括号, && 和 ||条件很容易出错(有人在不同的答案中说OP中的代码中的括号中有错误,也许它已被更正)。当然,适当的对齐会有所帮助。但是有什么好处呢?更具可读性?更容易维护?我不这么认为。当然只是我的意见。可以肯定的是,我真的很讨厌代码中有很多 ifs。
我已经将它包装在 if($bValue1) 中,因为这总是正确的,从技术上讲允许一些小的性能改进(尽管我们在这里谈论的数量可以忽略不计)。
FWIW:只有 2 个场景:前 2 个场景相同,不依赖于 bValue4
A
Anders

我的目标是简单性和可读性。

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

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

确保用描述性的东西替换场景的名称以及标志的名称。如果它对您的特定问题有意义,您可以考虑以下替代方案:

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

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

这里重要的不是谓词逻辑。它描述了您的领域并清楚地表达了您的意图。这里的关键是给所有输入和中间变量起好名字。如果您找不到好的变量名,则可能表明您以错误的方式描述问题。


+1这也是我会做的。就像@RedFilter 指出的那样,与公认的答案相比,这是自我记录的。在单独的步骤中为场景赋予自己的名称更具可读性。
P
P.W

我们可以使用 Karnaugh map 并将您的场景简化为一个逻辑等式。我已将 Online Karnaugh map 求解器与 4 个变量的电路一起使用。

https://i.stack.imgur.com/wlO2Q.png

这产生:

https://i.stack.imgur.com/HNAvs.png

A, B, C, D 更改为 bValue1, bValue2, bValue3, bValue4,这不过是:

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

因此,您的 if 语句变为:

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

当您有许多变量和许多应该评估为真的条件时,卡诺图特别有用。

在将真实场景简化为逻辑方程式后,添加指示真实场景的相关评论是一种很好的做法。


虽然在技术上是正确的,但该代码需要大量注释才能在几个月后由另一个开发人员进行编辑。
@ZdeslavVojkovic:我只想在方程式中添加一条评论。 //!(ABC + AB'C'D') (By K-Map logic)。如果开发人员还不了解 K-Maps,那将是他学习 K-Maps 的好时机。
我同意这一点,但 IMO 的问题是它没有清楚地映射到问题域,即每个条件如何映射到特定场景,这使得更改/扩展变得困难。当有 EF 条件以及 4 个新场景时会发生什么?正确更新此 if 语句需要多长时间?代码审查如何检查它是否正常?问题不在于技术方面,而在于“业务”方面。
我认为您可以将 A: ABC + AB'C'D' = A(BC + B'C'D') 分解(这甚至可以分解为 A(B ^ C)'(C + D'),尽管我会小心地将其称为“简化”)。
@PW该评论似乎与代码一样可以理解,因此有点毫无意义。更好的评论将解释您实际上是如何得出该等式的,即该语句应触发 TTTT、TTTF 和 TFFF。到时候你还不如把这三个条件写在代码里,根本不需要解释。
A
Andrew Truckle

这里真正的问题是:当另一个开发人员(甚至作者)必须在几个月后更改此代码时会发生什么。

我建议将其建模为位标志:

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

如果有更多场景或更多标志,则表方法比使用标志更具可读性和可扩展性。支持新方案只需要表格中的另一行。

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

不是最可维护的,但绝对简化了 if 条件。因此,在 imo 中,围绕按位运算留下一些评论将是绝对必要的。
IMO,表格是最好的方法,因为它可以更好地扩展额外的场景和标志。
我喜欢您的第一个解决方案,易于阅读且易于修改。我将进行 2 项改进: 1:将值分配给scenarioX,并明确指示使用的布尔值,例如SCENARIO_2 = true << 3 | true << 2 | true << 1 | false; 2:避免使用 SCENARIO_X 变量,然后将所有可用场景存储在 <std::set<int> 中。添加一个场景就像 mySet.insert( true << 3 | false << 2 | true << 1 | false; 一样,对于 3 个场景来说可能有点矫枉过正,OP 接受了我在回答中建议的快速、肮脏和简单的解决方案。
如果您使用的是 C++14 或更高版本,我建议您改为使用二进制文字作为第一个解决方案 - 0b1111、0b1110 和 0b1000 更清晰。您也可以使用标准库 (std::find?) 稍微简化一下。
我发现这里的二进制文字将是使第一个代码干净的 minimal 要求。以目前的形式,它是完全神秘的。描述性标识符可能会有所帮助,但我什至不确定。事实上,产生 scenario 值的位操作让我觉得不必要地容易出错。
G
Gian Paolo

我之前的答案已经是公认的答案,我在这里添加了一些我认为既可读又容易并且在这种情况下对未来修改开放的内容:

从@ZdeslavVojkovic 的回答(我觉得很好)开始,我想出了这个:

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

在工作中看到它here

好吧,这就是我通常追求的“优雅且可维护”(恕我直言)解决方案,但实际上,对于 OP 案例,我之前的“一堆 ifs”答案更适合 OP 要求,即使它既不优雅也不可维护。


您知道您可以随时编辑您的 previous answer 并进行改进。
S
Stack Danny

我还想提交另一种方法。

我的想法是将布尔值转换为整数,然后使用可变参数模板进行比较:

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

请注意该系统如何支持多达 32 个布尔值作为输入。将 unsigned 替换为 unsigned long long(或 uint64_t)将支持增加到 64 个案例。如果您不喜欢 if (summary != 0b1111u && summary != 0b1110u && summary != 0b1000u),您还可以使用另一种可变参数模板方法:

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

我喜欢这种方法,除了 main 函数的名称:“from bool ... to what?” — 为什么不明确地使用 bitmap_from_boolsbools_to_bitmap
是的@KonradRudolph,我想不出更好的名字,除了 bools_to_unsigned。位图是一个很好的关键字;编辑。
我想你想要summary!= 0b1111u &&...。如果 b != ca != b || a != c 始终为真
A
Andreas

这是一个简化版本:

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

注意,当然,这个解决方案比原来的解决方案更模糊,它的含义可能更难理解。

更新:评论中的 MSalters 发现了一个更简单的表达式:

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

是的,但很难理解。不过谢谢你的建议。
我将编译器简化表达式的能力与您的简化作为参考进行了比较:compiler explorer。 gcc 没有找到您的最佳版本,但它的解决方案仍然很好。 Clang 和 MSVC 似乎没有执行任何布尔表达式简化。
@AndrewTruckle:请注意,如果您需要更具可读性的版本,请这样说。您已经说过“简化”,但您接受了比原始版本更详细的版本。
simple 确实是一个模糊的术语。许多人在这种情况下将其理解为开发人员更容易理解,而不是编译器生成代码,因此更冗长确实可以更简单。
@IsmaelMiguel:当针对术语数量优化逻辑公式时,通常会丢失原始含义。但是可以在它周围发表评论,所以很清楚它的作用。即使对于已接受的答案,评论也不会有害。
Y
Yakk - Adam Nevraumont

考虑将您的表格尽可能直接地翻译到您的程序中。根据表格驱动程序,而不是用逻辑模仿它。

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

现在

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

这尽可能直接将您的真值表编码到编译器中。

Live example

您也可以直接使用 std::any_of

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

编译器可以内联代码,消除任何迭代并为您构建自己的逻辑。同时,您的代码准确地反映了您对问题的理解。


第一个版本非常易于阅读且易于维护,我真的很喜欢它。第二个更难阅读,至少对我来说是这样,并且要求 C++ 技能水平可能高于平均水平,肯定超过我的水平。不是每个人都能写出来的。刚学到新东西,谢谢
A
Andrew Truckle

我只在这里提供我的答案,就像有人建议展示我的解决方案的评论一样。我要感谢大家的见解。

最后,我选择添加三个新的“场景”boolean 方法:

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

然后我可以像这样应用我的验证程序:

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

在我的实时应用程序中,4 个布尔值实际上是从 DWORD 中提取的,该 DWORD 中编码了 4 个值。

再次感谢大家。


感谢分享解决方案。 :) 它实际上比复杂的 if 条件地狱要好。也许您仍然可以以更好的方式命名 INCLUDE_ITEM1 等,并且你们都很好。 :)
@HardikModha 好吧,从技术上讲,它们是“学生物品”,标志是为了表明它们是否要“包含”。所以我认为这个名字虽然听起来很笼统,但在这种情况下实际上是有意义的。 :)
A
Andrew Truckle

尽管 OP 的解决方案正是这样做的,但我没有看到任何回答来命名场景。

对我来说,最好将每个场景的注释封装到变量名或函数名中。您更有可能忽略评论而不是名称,如果您的逻辑在未来发生变化,您更有可能更改名称而不是评论。您不能重构评论。

如果您计划在您的函数之外(或可能想要)重用这些场景,则创建一个函数来说明其评估的内容(constexpr/noexcept 可选但推荐):

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

如果可能,请制作这些类方法(如在 OP 的解决方案中)。如果您认为不会重用逻辑,则可以在函数内部使用变量:

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;

编译器很可能会发现如果 bValue1 为假,那么所有场景都是假的。不要担心让它快速,只要正确和可读。如果您分析您的代码并发现这是一个瓶颈,因为编译器在 -O2 或更高时生成了次优代码,然后尝试重写它。


我比 Gian Paolo 的(已经不错的)解决方案更喜欢这个:它避免了控制流和使用被覆盖的变量 - 更具功能性的风格。
h
hessam hedieh

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

这种方法是可扩展的,就像有效条件的数量增加一样,您只需将更多的条件添加到场景列表中即可。


不过,我很确定这是错误的。它假定编译器仅对 true 使用单个二进制表示。使用“任何非零都为真”的编译器会导致此代码失败。请注意,true 必须转换1,它只是不需要像这样存储
@MSalters,tnx,我明白你的意思,我知道,有点像 2 is not equal to true but evaluates to true,我的代码不会强制 int 1 = true 并且只要所有 true 都转换为相同的 int 值就可以工作,所以这是我的问题:为什么编译器在将 true 转换为底层 int 时应该随机执行,您能详细说明一下吗?
执行 memcmp 来测试布尔条件是不是 C++ 方式,我也相当怀疑它是否是已建立的 C 方式。
@hessamhedieh:逻辑中的问题是“将 true 转换为底层 int”。这不是编译器的工作方式,
您的代码将复杂性从 O(1) 增加到 O(n)。不是用任何语言都可以使用的方法 - 撇开 C/C++ 不谈。
M
Michał Łoś

很容易注意到前两个场景是相似的——它们共享大部分条件。如果你想选择你目前所处的场景,你可以这样写(这是修改后的@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

更进一步,您会注意到,第一个布尔值必须始终为真,这是一个入口条件,因此您最终可以得到:

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
}

更重要的是,您现在可以清楚地看到,bValue2 和 bValue3 在某种程度上是相互关联的——您可以将它们的状态提取到一些具有更合适名称的外部函数或变量中(尽管这并不总是容易或合适的):

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
}

这样做有一些优点和缺点:

条件更小,所以更容易推理它们,

更容易进行漂亮的重命名以使这些条件更易于理解,

但是,他们需要了解范围,

而且它更僵硬

如果您预测上述逻辑会发生变化,则应使用 @gian-paolo 中提出的更直接的方法。

否则,如果这些条件已经确立,并且是一种永远不会改变的“可靠规则”,请考虑我的最后一个代码片段。


g
gsamaras

根据 mch 的建议,您可以这样做:

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

其中第一行涵盖了前两个好的案例,第二行涵盖了最后一个。

Live Demo,我在那里玩过,它通过了你的案例。


M
Matt

@GianPaolo 的好答案略有不同,有些人可能会觉得更容易阅读:

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

s
shogged

每个答案都过于复杂且难以阅读。对此的最佳解决方案是 switch() 语句。它既可读又使添加/修改其他案例变得简单。编译器也擅长优化 switch() 语句。

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

您当然可以在 case 语句中使用常量并将它们组合在一起以提高可读性。


作为一个老 C 程序员,我会定义一个“PackBools”宏并将其用于“switch(PackBools(a,b,c,d))”和案例,例如直接“case PackBools(true , true...)" 或将它们定义为局部常量。例如 "const unsigned int scenario1 = PackBools(true, true...);"
G
Gnudiff

为了清楚起见,我还会使用快捷方式变量。如前所述,场景 1 等于场景 2,因为 bValue4 的值不会影响这两个场景的真实性。

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

然后你的表情变得:

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

为 MAJORTRUE 和 MAJORFALSE 变量(以及实际上为 bValue* 变量)赋予有意义的名称将有助于提高可读性和维护性。


佚名

关注问题的可读性,而不是特定的“if”语句。

虽然这会产生更多的代码行,但有些人可能认为它过分或不必要。我建议从特定的布尔值中抽象出你的场景是保持可读性的最佳方式。

通过将事物拆分为具有可理解名称的类(随意使用函数或您喜欢的任何其他工具)——我们可以更轻松地展示每个场景背后的含义。更重要的是,在具有许多移动部件的系统中 - 更容易维护和加入现有系统(同样,尽管涉及多少额外代码)。

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

在某些时候,冗长开始损害可读性。我认为这太过分了。
@JollyJoker 在这种特定情况下我确实同意 - 但是,从 OP 将所有内容命名为非常通用的方式,我的直觉是,他们的“真实”代码可能比他们给出的示例复杂得多。真的,我只是想把这个替代方案放在那里,因为这是我为更复杂/涉及的事情构建它的方式。但你是对的——对于 OPs 的具体例子,它过于冗长并且让事情变得更糟。
i
ispiro

这取决于它们代表什么。

例如,如果 1 是一个键,而 23 是必须同意的两个人(除非他们同意他们需要的 NOT第三人称 - 4 - 确认)最易读的可能是:

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

应大众要求:

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

您可能是对的,但使用数字来说明您的观点会影响您的答案。尝试使用描述性名称。
@jxh这些是OP使用的数字。我刚刚删除了 bValue
@jxh 我希望现在好多了。
D
Derviş Kayımbaşıoğlu

进行按位运算看起来非常干净易懂。

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

按位比较对我来说是可读的。另一方面,构图看起来很人工。
m
mesibo

为了清楚起见,我用 a、b、c、d 表示,用 A、B、C、D 表示补语

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

方程

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

使用任何适合您的方程式。


O
Owen Meyer
If (!bValue1 || (bValue2 != bValue3) || (!bValue4 && bValue2))
{
// you have a problem
}

b1 必须始终为真

b2 必须始终等于 b3

如果 b2(和 b3)为真,则 b4 不能为假

简单的


F
François Gueguen

只是个人对公认答案的偏好,但我会写:

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

T
Tezra

首先,假设您只能修改场景检查,我将专注于可读性并将检查包装在一个函数中,这样您就可以调用 if(ScenarioA())

现在,假设您确实想要/需要优化它,我建议将紧密链接的布尔值转换为常量整数,并在它们上使用位运算符

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
}

这使得表达场景就像列出其中的一部分一样简单,允许您使用 switch 语句跳转到正确的条件,并使以前没有见过的开发人员感到困惑。 (C# RegexOptions 用这个模式设置标志,不知道有没有c++库的例子)


实际上,我使用的不是四个布尔值,而是一个带有四个嵌入式布尔值的 DWORD。现在改太晚了。不过谢谢你的建议。
s
sardok

对于某些人来说,嵌套的 if 可能更容易阅读。这是我的版本

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

就个人而言,如果可能的话,我通常会避免嵌套 if 语句。虽然这种情况很好且可读,但一旦添加了新的可能性,嵌套就会变得非常难以阅读。但是,如果场景永远不会改变,它绝对是一个很好且可读的解决方案。
@Dnomyar96 我同意。我个人也避免嵌套ifs。有时,如果逻辑很复杂,将其分解成碎片对我来说更容易理解。例如,一旦你输入了bValue1块,那么你就可以把里面的所有东西都当作你心理过程中的一个新的页面。我敢打赌,解决问题的方式可能非常个人化,甚至是文化问题。
f
fralau

这个问题已经给出了几个正确的答案,但我会采取不同的观点:如果代码看起来太复杂,那么有些地方不太对劲。代码将难以调试,更有可能是“一次性使用”。

在现实生活中,当我们发现这样的情况时:

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

当四个状态通过如此精确的模式连接时,我们正在处理模型中某个“实体”的配置。

一个极端的比喻是我们将如何在模型中描述“人类”,如果我们不知道他们作为具有连接到特定自由度的组件的单一实体存在:我们将不得不描述“躯干”的独立状态, “手臂”、“腿”和“头”,这会使理解所描述的系统变得复杂。直接结果将是异常复杂的布尔表达式。

显然,降低复杂性的方法是抽象,而 C++ 中选择的工具是对象范式。

所以问题是:为什么会有这样的模式?这是什么,它代表什么?

由于我们不知道答案,我们可以求助于数学抽象:数组:我们有三个场景,现在每个场景都是一个数组。

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

此时您已完成初始配置。作为一个数组。例如 std::array 有一个相等运算符:

此时您的语法变为:

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

}

正如 Gian Paolo 的回答一样,它简短、清晰且易于验证/调试。在这种情况下,我们将布尔表达式的细节委托给了编译器。


j
jamesdlin

如果您摆脱布尔标志,您将不必担心布尔标志的无效组合。

可接受的值为: 场景 1 |情景 2 |场景 3 bValue1:真 |真实 |真 bValue2:真 |真实 |假 bValue3:真 |真实 |假 bValue4:真 |假 |错误的

你显然有三种状态(场景)。最好对其进行建模并从这些状态中导出布尔属性,而不是相反。

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

这肯定比 Gian Paolo's answer 中的代码更多,但根据您的情况,这可能更易于维护:

如果添加了额外的布尔属性或场景,则需要修改一组中心函数。添加属性只需要添加一个函数。如果添加场景,在 switch 语句中启用关于未处理枚举案例的编译器警告将捕获不处理该场景的属性获取器。

添加属性只需要添加一个函数。

如果添加场景,在 switch 语句中启用关于未处理枚举案例的编译器警告将捕获不处理该场景的属性获取器。

如果您需要动态修改布尔属性,则无需在任何地方重新验证它们的组合。您无需切换单个布尔标志(这可能导致标志的无效组合),而是拥有一个从一种场景转换到另一种场景的状态机。

这种方法还具有非常高效的副作用。


J
Jim Cullen

当您只有 3 个案例并且每个案例的逻辑都很简单时,接受的答案就很好。

但是,如果每个案例的逻辑更复杂,或者有更多案例,则更好的选择是使用 chain-of-responsibility 设计模式。

您创建一个 BaseValidator,其中包含对 BaseValidator 的引用和对 validate 的方法,以及在引用的验证器上调用验证的方法。

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

然后,您创建多个继承自 BaseValidator 的子类,用每个验证器所需的逻辑覆盖 validate 方法。

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

然后使用它很简单,实例化每个验证器,并将每个验证器设置为其他验证器的根:

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

本质上,每个验证案例都有自己的类,负责 (a) 确定验证是否匹配该案例,以及 (b) 如果不匹配,则将验证发送给链中的其他人。

请注意,我不熟悉 C++。我试图匹配我在网上找到的一些示例的语法,但如果这不起作用,请将其视为伪代码。我在下面还有一个完整的 Python 示例,如果愿意,可以用作基础。

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

同样,对于您的特定示例,您可能会发现这种过度杀伤力,但如果您最终需要满足一组更复杂的案例,它会创建更清晰的代码。


K
Kobap Bopy
if(!bValue1)
    return false;
if(bValue2 != bValue3)
    return false;
if(bValue3 == false && bValuer4 == true)
    return false;
return true;

A
Andrew Truckle

我的 2 美分:声明一个变量 sum (integer) 以便

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

根据您想要的条件检查 sum,仅此而已。

通过这种方式,您可以在将来轻松添加更多条件,使其非常易于阅读。


h
hedzr

使用位域:

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:

这对 CPPers 来说是一个很大的遗憾。但是,UB 不是我担心的,请在 http://coliru.stacked-crooked.com/a/2b556abfc28574a1 处查看。


由于访问非活动联合字段,这会导致 UB。
形式上它是 C++ 中的 UB,您不能设置一个联合成员并从另一个成员读取。从技术上讲,为整数值位实现模板化 getter\setter 可能会更好。
我认为如果要将联合的地址转换为 unsigned char*,行为将转变为实现定义,尽管我认为简单地使用类似 ((((flag4 <<1) | flag3) << 1) | flag2) << 1) | flag1 的东西可能会更有效。