标准库或升压中是否有任何有助于有条件地执行函数的东西?

Is there anything from the standard library or boost that facilitates conditionally executing a function?

提问人:DXZ 提问时间:9/8/2022 最后编辑:DXZ 更新时间:9/9/2022 访问量:93

问:

我正在重构一个具有太多 if-else 的函数,类似于以下内容但更复杂。此功能的一些主要特征是:

  1. 它为许多前提条件(例如,和 )提前纾困。condition1()condition2()
  2. 它只在非常具体的场景(例如,和 )上做一些有意义的事情。(哦,是的,临时错误修复的美妙之处!doA()doB()
  3. 某些前提条件可能独立于其他条件,也可能不独立于其他条件(例如,)。condition3/4/5/6()
retT foo() { // total complexity count = 6
    if (!condition1()) { // complexity +1
        return retT{};
    }

    if (!condition2()) { // complexity +1
        return retT{};
    }

    if (condition3()) { // complexity +1
        if (condition4() || condition5()) { // complexity +2
            return doA();
        }
        else if (condition6()) { // complexity +1
            return doB();
        }
    }

    return retT{};
}

目标是根据它们的精确条件调用这些实际作品,而不是让它们容易受到 if-else 结构变化的影响。更具体地说,我想变成这样的东西:foo()foo()

retT foo() { // total complexity count = 4
    ConditionalCommand<retT> conditionalDoA{doA};
    conditionalDoA.addCondition(condition1());
    conditionalDoA.addCondition(condition2());
    conditionalDoA.addCondition(condition3());
    conditionalDoA.addCondition(condition4() || condition5()); // complexity +1
    
    ConditionalCommand<retT> conditionalDoB{doB};
    conditionalDoB.addCondition(condition1());
    conditionalDoB.addCondition(condition2());
    conditionalDoB.addCondition(condition3());
    conditionalDoB.addCondition(!(condition4() || condition5())); // complexity +2
    conditionalDoB.addCondition(condition6());

    for (auto& do : {conditionalDoA, conditionalDoB}) {
        if (do()) { // complexity +1
            return do.result();
        }
    }

    return retT{};
}

这使得实现更加线性,并且执行特定工作的条件更加明确。我知道这相当于为每个工作创建一个第一级 if 子句,并列出了所有添加的条件,但上面的代码会:

  • 减少内部复杂度度(if-else、逻辑运算符和基于三元,如代码注释所示),
  • 防止将来新开发人员侵入第一级 if 子句,例如,谁想要而不是 if 是 ,并且doC()doA()condition7()true
  • 请允许我独立于其他作品的条件来完善每个作品的条件(回想一下,有些条件可能相互依赖)。

所以问题是,是否有任何现有的 std 或 boost 实用程序可以做什么,所以我不需要重新发明轮子?ConditionalCommand

C++ Boost 标准

评论

2赞 Jarod42 9/9/2022
“减少我们的内部复杂性测量”。这将是“隐藏”,而不是真正的“减少”。
2赞 Sam Varshavchik 9/9/2022
你真的认为这个提议的语法比原始代码更容易阅读和理解吗?
1赞 NathanOliver 9/9/2022
就我个人而言,我喜欢原始代码。尝试获得与原样相同的性能即使不是不可能,也是困难的。如果是“代码太多”,那么可以将其重构为多个函数。
1赞 john 9/9/2022
不得不同意,在这两个例子中,我更喜欢原版,
2赞 Useless 9/9/2022
也许你的圈复杂度真的太高了,需要重构,也许过程重构真的行不通......我现在仍然不会选择你的建议——这似乎与你自己的要求相矛盾。具体而言,条件之间的依赖关系不受尊重。

答:

5赞 Useless 9/9/2022 #1

编辑:结论在顶部,框架挑战在下面。

回到最初的问题,std 或 boost 中是否存在任何可以做什么的东西?ConditionalCommand

好吧,如果你真的不担心这个设计违反了你自己规定的要求,答案是:。没有什么做到这一点。

但是,您可以写类似

std::array condA { condition1(), condition2(), condition3(),
                  (condition4() || condition5()) };
if (std::ranges::all_of(condA, std::identity{})) doA();

if (std::ranges::all_of(
    std::initializer_list<bool>{
      condition1(), condition2(), condition3(),
      (condition4() || condition5()),
      condition6()
    },
    std::identity{})
   )
  doB();
  

或者任何你喜欢的东西。你只是建议在这个逻辑上有一个非常薄的便利层。


这使得实现更加线性

在完全线性的控制流和完美的线性数据结构之间,我真的看不出这个标准有任何优势。

更明确地执行特定工作的条件

如果“更明确”的意思是“更声明性”,那么我猜是这样。不过,你已经将实际发生的所有事情都隐藏在一些神秘的模板中,所以它最好非常清晰、直观且有据可查。

减少内部复杂度度(if-else、逻辑运算符和基于三元,如代码注释所示),

坦率地说,你的“内部复杂性测量”是愚蠢的。如果你针对一个糟糕的目标进行优化,你会得到一个糟糕的结果。

在这里,你明显增加了整体复杂性,增加了新开发人员的学习曲线,使条件与其后果之间的关系变得不那么清晰,控制流更难调试。

但是你已经以一种你的“内部复杂性测量”选择忽略的方式做到了这一点,所以它看起来像是一种改进。

虽然我不喜欢将圈复杂度作为一个宽泛的衡量标准,但如果你的复杂度真的比问题中显示的要高得多,那么在考虑你的建议之前,我仍然会尝试重构过程代码。

防止新开发人员将来侵入第一级 if 子句,例如,谁想要而不是 if isdoC()doA()condition7()true

只需为您的 7 个条件的每个组合编写单元测试(或运行每个排列的单个测试),并让您的初级开发人员自己发现 CI 服务器何时抱怨他们的分支。

你不是通过这样混淆你的代码来帮助他们变得不那么初级,你是在试图以一种实际上并不能帮助他们改进的方式将自己与他们的错误隔离开来。

此外,原始控制流甚至可能存在错误

在这种情况下,您绝对应该编写测试用例!你说的是重构你不信任的代码,这种方式违反了你自己声明的要求,而且没有办法验证结果。

请允许我独立于其他作品的条件来完善每个作品的条件(回想一下,有些条件可能相互依赖)。

如果您确实想要一种不太容易出错的组织方式,则应显式编码这些条件相互依赖关系。目前,您仍然可以通过以错误的顺序添加条件来破坏所有内容。

此外,您当前无条件地执行所有条件,但条件 4 和 5 的短路评估除外。这甚至定义明确吗?是否保证保持明确定义?

此外,您现在正在多次评估每个条件,每个可能的操作一次。

如果你真的必须在数据而不是代码中对此进行编码,它可能类似于显式依赖关系图(因此依赖于并且永远不会执行,除非该依赖项的计算结果为 )。然后,您可以将多个叶操作附加到同一图形,并且不需要任何冗余的重新计算。condition2condition1true

公平地说,实现它很痛苦,但至少它满足了你的依赖要求。

0赞 Enlico 9/9/2022 #2

我同意所有其他人的观点,即您的尝试只是将复杂性隐藏在一个可读性更强的空白下。我发现它比原版更难读

我认为你唯一的希望是查看原始代码并解开它。

在您的简化示例中,您的语句多于可能的返回值,我敢打赌,在原始示例中,您的 s 也远远多于可能的返回值。那么,你为什么不简单地(用笔和纸)绘制一张地图,说明什么条件会导致什么返回值呢?returnreturn

举个简单的例子,

  • retT{}仅当!c1 || !c2 || !c3 || (!c4 && !c5 && !c6)
  • doA()仅当c1 && c2 && c3 && (c4 || c5)
  • doB()仅当c1 && c2 && c3 && c6

请注意,可以使用德摩根定律重写第一个布尔值,例如 .!(c1 && c2 && c3 && (c4 || c5 || c6))

根据观察结果,唯一重要的布尔值实际上是这些:

bool b1 = c1 && c2 && c3;
bool b2 = c4 || c5;
bool b3 = c6;

然后,您可以将逻辑重写为

  • doA()仅当 时返回 ,调用它b1 && b2p1
  • doB()仅当 时返回 ,调用它b1 && b3p2
  • retT{}仅当!(b1 && (b2 || b3)) = !((b1 && b2) || !(b1 && b3)) = !(b1 && b2) && !(b1 && b3) = !p1 && !p2

等式的最后一个序列是验证所有分支确实导致 a(否则代码将是错误的)。return

考虑到上述情况(并在纸面上),代码可以简化如下:

bool b1 = c1 && c2 && c3;
bool b2 = c4 || c5;
bool b3 = c6;

if (b1) {
  if (b2) {
    return doA();
  }
  if (b3) {
    return doB();
  }
}
return retT{};

或者,如果你真的害怕计算布尔值所需的时间,你可以计算而不是“懒惰”:

bool b1 = c1 && c2 && c3;

if (b1) {
  bool b2 = c4 || c5;
  if (b2) {
    return doA();
  }
  //bool b3 = c6;
  if (/*b3*/c6) {
    return doB();
  }
}
return retT{};