什么是更好的方法来避免do-while(0); hack in C++?

San*_*alp 232 c++ do-while

当代码流是这样的:

if(check())
{
  ...
  ...
  if(check())
  {
    ...
    ...
    if(check())
    {
      ...
      ...
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

我一般都看到这个工作,以避免上面的凌乱的代码流:

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);
Run Code Online (Sandbox Code Playgroud)

有哪些更好的方法可以避免这种解决方案/黑客攻击,从而使其成为更高级别(行业级别)的代码?

任何开箱即用的建议都是受欢迎的!

Mik*_*ail 308

在函数中隔离这些决策并使用returns代替breaks,这被认为是可接受的做法.虽然所有这些检查都对应于与函数相同的抽象级别,但这是非常合乎逻辑的方法.

例如:

void foo(...)
{
   if (!condition)
   {
      return;
   }
   ...
   if (!other condition)
   {
      return;
   }
   ...
   if (!another condition)
   {
      return;
   }
   ... 
   if (!yet another condition)
   {
      return;
   }
   ...
   // Some unconditional stuff       
}
Run Code Online (Sandbox Code Playgroud)

  • +1.这也是一个很好的答案.在C++ 11中,*isolated函数*可以是lambda,因为它也可以捕获局部变量,因此简化了操作! (35认同)
  • @Damon如果有的话,`return`更干净,因为任何读者都会立即意识到它正常工作以及它做了什么.使用`goto`,你必须环顾四周才能看到它的用途,并确保没有错误.伪装*是利益. (24认同)
  • @MatsPetersson:"函数中的隔离"意味着重构为只执行测试的新函数. (22认同)
  • @SigTerm:有时代码应该移动到一个单独的函数,只是为了将每个函数的大小保持在一个容易理解的大小.如果您不想为函数调用付费,请将其标记为`forceinline`. (11认同)
  • @deworde:根据情况,这个解决方案可能比goto更长,更不易读.不幸的是,因为C++不允许本地函数定义,所以你必须在其他地方移动该函数(你要返回的函数),这会降低可读性.它可能最终会有几十个参数,因为有几十个参数是一件坏事,你将决定将它们包装成struct,这将创建新的数据类型.对于简单的情况打字太多了. (4认同)
  • 我认为这不一定比'goto`更清洁或更好(除了出于宗教原因),因为它只是将`goto`伪装成`return`(另外在堆栈上推送/弹出args!)就像伪装它一样在原始代码中使用`break`.然而,+1以非常有创意的方式以合法的方式使用/滥用语言. (4认同)
  • @Nawaz:虽然我更喜欢这个而不是`goto`,但是在*long*lambda中将它包装起来比`do {}更糟糕而在(0);在我看来.长lambdas更难读,因为很快就会混淆函数`return`与(lambda或包含函数?)相关联. (4认同)
  • @ R.MartinhoFernandes:无论如何,我认为有些人忘记了函数/ lambdas/what的意思是不要避免goto,而是要提高可读性,防止错误,避免代码重复.(意见)重复代码模式意味着应该有一个功能.如果该模式仅在一个函数中使用,则它应该是lambda.但是,如果pattern不重复,或者一个函数只有一个调用者,则可能表明没有必要将这段代码转换为单独的函数.以同样的方式,只被调用一次的lambda是没有意义的. (4认同)
  • @ R.MartinhoFernandes:这位家伙建议把潜在的巨大代码块塞进单个lambda中,只有这个函数才会调用它.**只是**以避免转到.这应该是一个更好的解决方案吗?除非您在多个场景中多次使用代码块,否则应该没有lambdas. (3认同)
  • 这假设您实际上是在函数中完成,而是在当前代码块之后有另一个块来处理(例如清理或找到结果的最终处理). (2认同)
  • @SigTerm:当然更长(goto很短),但你的"数十个参数"已经阻止了任何可读性愿望.在维护方面,goto会变得更糟,因为你必须检查多条路径才能解决任何问题,而这样,"输入正确?输出错误?错误在func内部.钻孔." 当然,你可以清理它*更多*,但作为补丁和修复,更喜欢返回到goto. (2认同)
  • @SigTerm看来你没有看到Nawaz在你上面的评论. (2认同)
  • @ user1354557有用于清理目的的RAII. (2认同)

Mat*_*son 254

有时候使用goto实际上是正确的答案 - 至少对于那些没有在宗教信仰中长大的人来说," goto无论问题是什么都不能成为答案" - 这就是其中一个案例.

此代码使用黑客攻击do { ... } while(0);的唯一目的是打扮goto一个break.如果你打算使用goto,那么就开放吧.让代码HARDER阅读是没有意义的.

特殊情况就是当你有很多具有相当复杂条件的代码时:

void func()
{
   setup of lots of stuff
   ...
   if (condition)
   {
      ... 
      ...
      if (!other condition)
      {
          ...
          if (another condition)
          {
              ... 
              if (yet another condition)
              {
                  ...
                  if (...)
                     ... 
              }
          }
      }
  .... 

  }
  finish up. 
}
Run Code Online (Sandbox Code Playgroud)

它实际上可以通过没有这么复杂的逻辑使代码正确的清除.

void func()
{
   setup of lots of stuff
   ...
   if (!condition)
   {
      goto finish;
   }
   ... 
   ...
   if (other condition)
   {
      goto finish;
   }
   ...
   if (!another condition)
   {
      goto finish;
   }
   ... 
   if (!yet another condition)
   {
      goto finish;
   }
   ... 
   .... 
   if (...)
         ...    // No need to use goto here. 
 finish:
   finish up. 
}
Run Code Online (Sandbox Code Playgroud)

编辑:为了澄清,我决不建议使用goto作为一般解决方案.但有些情况下,goto解决方案比其他解决方案更好.

想象一下,例如我们正在收集一些数据,并且正在测试的不同条件是某种"这是收集的数据的结束" - 这取决于某种"继续/结束"标记,这取决于哪里你在数据流中.

现在,当我们完成后,我们需要将数据保存到文件中.

是的,通常有其他解决方案可以提供合理的解决方案,但并非总是如此.

  • 不同意.`goto`可能有一个地方,但`goto cleanup`没有.使用RAII进行清理. (76认同)
  • 所以,继续在这一点上投票,大概来自那些"goto"的宗教信仰,从来都不是正确的答案.如果有评论,我会很感激... (25认同)
  • 人们讨厌`goto`,因为你必须考虑使用它/了解使用它的程序......另一方面,微处理器是建立在`jumps`和`条件跳转`...所以问题在于有些人,不是逻辑或其他东西. (19认同)
  • +1适当使用`goto`.如果预计会出现错误(非例外),则RAII不是正确的解决方案,因为这会滥用例外情况. (17认同)
  • @MSalters:这假设清理涉及可以用RAII解决的问题.也许我应该说"问题错误"或其他一些. (14认同)
  • 我必须承认,我对`goto`的使用往往是C代码,而不是C++代码.在这种情况下,RAII并没有真正起作用.我已经处理了相当多的驱动程序代码,它往往是用C语言编写的,不是因为它在C++中不能编写得那么好,而是因为在大多数驱动程序运行时环境中都不允许使用C++(例如Linux和直到最近至少Windows).因此,即使驱动程序是为驱动程序中支持C++的操作系统编写的,也可能不会使用C++来使其可移植. (14认同)
  • @woliveirajr:这就是为什么我认为程序员应该从汇编语言(甚至是8086)开始,而tehn会转向更高层次的概念. (6认同)
  • 这也是盲目地在每个单语句块周围放置`{...}`会使代码更难读的情况. (6认同)
  • @woliveirajr更像是函数调用,条件块,线程,进程调度或中断,所有这些都是在硬件,操作系统或语言中完成的!"goto"的问题不是跳跃,而是它有助于编写不清楚的代码,因为你可以(理论上)从任何地方跳到任何地方,导致任何人都可以随时想到的任何混乱(好吧,我想我已经完成了"任何"现在`:)`).使用得很好,它可以轻松*提高*至少一些替代技术的可读性,以及专业程序员希望代码的可读性低于可能的程度? (5认同)
  • @deworde:你知道,如果有更好的方法,你应该把它们作为答案发布.没有goto,do/while是最好的解决方案.RAII /异常不是最佳解决方案,因为它们模拟流量控制. (5认同)
  • 我投了这票,因为我更喜欢米哈伊尔给出的回归解决方案.如果执行正确完成,返回时会看得更快.而且,如果在团队中工作,这很重要,这可能是更常见的解决方案,可以节省讨论.我认为这是一个C解决方案,而不是utnapistim所说的C++解决方案. (5认同)
  • 我喜欢这个答案.它基本上将`goto`视为轻量级异常.我不确定这是C++的正确答案,但对C来说这是一个很好的答案. (5认同)
  • @SebastianK:不知道是否应该使用投票,因为有人喜欢或不喜欢某事......"在投票时投下相反信号:该帖子包含错误的信息,研究不足或无法传达信息" (5认同)
  • @BlackBear:没什么,真的.RAII简单地保证即使抛出异常也会清理对象(在堆栈展开期间调用析构函数),并且goto不应跳过对象初始化,因此您最终将使用简单类型或指针.Goto是一种可接受的技术,但它更像C,这就是为什么有些人不喜欢它. (4认同)
  • `//这里不需要使用goto.尽管如此.你永远不知道是否可以添加另一个`if`语句. (4认同)
  • @SebastianK我同意woliveirajr,保存为真正*错误*的答案的downvotes.您可能会也可能不会同意使用`goto`,但它确实回答了IMO的问题("有什么其他/更好的方法可以做到这一点?").当然,问题的"更好"部分本质上是主观的.(我常常发现自己倾向于使用不同技术或从不同角度回答问题的几个不同的答案;不需要向那些我不会亲自使用该技术的人投票,只要它们不是*错*.) (4认同)
  • 我一直认为goto是一个sympton而不是一个原因,而非全球州/单身人士.它标记需要重新思考的代码,尽管有时候这种重新思考从未完成,然后goto成为一种强制在其他地方使用可维护性较低的代码的真菌.这就是为什么人们对此有所了解 - 门需要快速关闭或保持开放状态. (3认同)
  • 如果我们谈论理论,那没关系.但事实是,许多人不会允许`goto`s,只因为他们是'goto`.如果你愿意,可以称之为"指令种族主义",但事实就是如此. (3认同)
  • 我不打算downvote,但是 - 我认为`do {...} while(false);`方法比基于`goto`的方法更好,因为那时你知道所有的变量都在你期望的范围,没有风险你不小心绕过变量的初始化,等等. (3认同)
  • @utnapistim如果完成代码没有进行任何清理,可以认为析构函数不是正确的使用方法.当然你可以使用RAII做任何事情,但如果没有"资源",那么"获取"或"释放"它就没有意义.例外情况可用于例外情况.虽然我可能会使用嵌套ifs或单独的函数/ lambda或mabye甚至是析构函数,但这并没有改变使用`goto`的有效性. (2认同)
  • @MichaelKjörling`goto`无法在C++(甚至C)中跳转到任何地方 (2认同)
  • @GiulioFranco:我问了一个问题(到目前为止,我对stackoverflow的独特问题,关于GOTO,它在不到一天的时间内关闭了...... http://stackoverflow.com/questions/18518632/why-and-when-应该使用G-word关闭使用G-word,我会说:) (2认同)

das*_*ght 82

您可以使用带bool变量的简单延续模式:

bool goOn;
if ((goOn = check0())) {
    ...
}
if (goOn && (goOn = check1())) {
    ...
}
if (goOn && (goOn = check2())) {
    ...
}
if (goOn && (goOn = check3())) {
    ...
}
Run Code Online (Sandbox Code Playgroud)

一旦checkN返回a,该执行链将立即停止false.check...()由于&&操作员的短路,不会再执行任何呼叫.此外,优化编译器足够聪明,可以识别设置goOnfalse单行道,并goto end为您插入缺失.结果,上面的代码的性能将与do/的相同while(0),只是没有对其可读性的痛苦打击.

  • @Mikhail只对未经训练的眼睛. (90认同)
  • `if`条件内的分配看起来_very_ suspicious. (30认同)
  • 我之前使用过这种技术,它总是让我感到烦恼,我认为编译器必须生成代码来检查每个`if`,无论什么`goOn`即使早期失败(而不是跳跃/爆发)...但我刚刚进行了测试,VS2012至少足够聪明,可以在第一次失误之后将所有内容短路.我会经常使用它.*注意:*如果你使用`goOn&= checkN()`那么`checkN()`总是会运行,即使`goOn`在`if`的开头是'false`(即,不要这样做) . (20认同)
  • @Nawaz:如果你有一个未经训练的头脑在代码库中进行任意改变,那么你有一个更大的问题,而不仅仅是`if`s中的赋值. (11认同)
  • @sisharp Elegance在旁观者的眼中是如此!我不明白在世界上如何滥用循环结构可以某种程度上被视为"优雅",但也许这只是我. (6认同)
  • @wallyk:每当我看到`do` ...`while(0)`构造时,我认为编写它的人有一个毫无根据的`goto`s恐惧症.如果您打算使用`goto`,请将其称为"goto",而不是将其打扮成循环. (5认同)
  • 通过使用这种延续模式,每次都会被击中.根据ifs的数量,这可能是不可取的,特别是如果这是在一些循环中并且处理器猜测某些ifs是错误的.我赞成只使用goto停止检查每一个if.哪个变成了Mats的答案 (3认同)
  • @sisharp这显然是不正确的:现代优化器将绕过每个`if`一次`goOn`设置为'false`(查看[mark的评论上面](http://stackoverflow.com/questions/18507518/what-are-some - 更好-方式对避免最DO-while0-劈在-C/18508168?noredirect = 1个#comment27217332_18508168)). (3认同)
  • @dasblinkenlight"只针对未经训练的眼睛"不,也可能是编译器.你的`if(goOn = check0())`可能会触发警告(类似"条件赋值"或"你的意思是'=='?"),需要将赋值括在括号中(`if((goOn = check0()))`)如果关闭则关闭. (2认同)

Mik*_*our 38

  1. 尝试将代码提取到单独的函数中(或者可能不止一个).如果检查失败,则从函数返回.

  2. 如果它与周围的代码紧密耦合来做到这一点,并且你找不到减少耦合的方法,请查看此块之后的代码.据推测,它清理了函数使用的一些资源.尝试使用RAII对象管理这些资源; 然后breakreturn(或者throw,如果那更合适的话)替换每个狡猾的东西,让对象的析构函数为你清理.

  3. 如果程序流程(必然)如此波动以至于你真的需要一个goto,那么就使用它而不是给它一个奇怪的伪装.

  4. 如果你有盲目禁止的编码规则goto,而你实际上无法简化程序流程,那么你可能不得不用你的do黑客来伪装它.

  • 我谦卑地提出,RAII虽然有用但不是灵丹妙药.当你发现自己要编写一个没有其他用途的转换goto-to-RAII类时,我绝对认为只要使用人们已经提到的"goto-of-the-world"成语,你就会更好. (4认同)
  • 我同意,但我认为编写goto-RAII转换类是一个坏主意,我认为应该明确说明. (3认同)

utn*_*tim 36

TLDR:RAII,事务代码(仅在已经计算时设置结果或返回内容)和异常.

答案很长:

C中,这种代码的最佳实践是在代码中添加EXIT/CLEANUP/other标签,其中发生本地资源的清理并返回错误代码(如果有的话).这是最佳实践,因为它将代码自然地分为初始化,计算,提交和返回:

error_code_type c_to_refactor(result_type *r)
{
    error_code_type result = error_ok; //error_code_type/error_ok defd. elsewhere
    some_resource r1, r2; // , ...;
    if(error_ok != (result = computation1(&r1))) // Allocates local resources
        goto cleanup;
    if(error_ok != (result = computation2(&r2))) // Allocates local resources
        goto cleanup;
    // ...

    // Commit code: all operations succeeded
    *r = computed_value_n;
cleanup:
    free_resource1(r1);
    free_resource2(r2);
    return result;
}
Run Code Online (Sandbox Code Playgroud)

在C中,在大多数代码库中,if(error_ok != ...goto代码通常隐藏在一些很方便的宏(RET(computation_result),ENSURE_SUCCESS(computation_result, return_code)等).

C++提供了超过C的额外工具:

  • 清理块功能可以实现为RAII,这意味着您不再需要整个cleanup块并允许客户端代码添加早期返回语句.

  • 无论何时你无法继续,你都会抛出,将所有if(error_ok != ...变成直接的呼叫.

等价的C++代码:

result_type cpp_code()
{
    raii_resource1 r1 = computation1();
    raii_resource2 r2 = computation2();
    // ...
    return computed_value_n;
}
Run Code Online (Sandbox Code Playgroud)

这是最佳做法,因为:

  • 它是显式的(即,当错误处理不明确时,算法的主要流程是)

  • 编写客户端代码非常简单

  • 它很小

  • 很简单

  • 它没有重复的代码构造

  • 它不使用宏

  • 它不使用奇怪的do { ... } while(0)结构

  • 它可以通过最小的努力重复使用(也就是说,如果我想将调用复制computation2();到另一个函数,我不必确保do { ... } while(0)在新代码中添加一个,也不必添加#definegoto包装器宏和清理标签,也不还要别的吗).


Pet*_*r R 21

为了完整起见,我正在添加一个答案.许多其他答案指出,大条件块可以分成单独的函数.但正如同时也指出的那样,这种方法将条件代码与原始上下文分开.这是在C++ 11中将lambdas添加到语言中的一个原因.其他人建议使用lambdas,但没有提供明确的样本.我在这个答案中加了一个.让我感到震惊的是,它do { } while(0)在许多方面与这种方法非常相似- 也许这意味着它仍然是goto伪装的....

earlier operations
...
[&]()->void {

    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
}();
later operations
Run Code Online (Sandbox Code Playgroud)

  • 对我来说,这个黑客看起来比做什么更糟糕...而黑客. (7认同)

kri*_*iss 18

当然不是答案,但答案(为完整起见)

代替 :

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);
Run Code Online (Sandbox Code Playgroud)

你可以写:

switch (0) {
case 0:
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}
Run Code Online (Sandbox Code Playgroud)

这仍然是一个跳转进行了伪装,但至少它不是一个循环了.这意味着你不必非常仔细地检查是否有一些继续隐藏在块中的某个地方.

该构造也很简单,您可以希望编译器将其优化.

正如@jamesdlin所建议的那样,你甚至可以将它隐藏在宏之后

#define BLOC switch(0) case 0:
Run Code Online (Sandbox Code Playgroud)

并使用它

BLOC {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}
Run Code Online (Sandbox Code Playgroud)

这是可能的,因为C语言语法需要在切换后的语句,而不是括号内的块,并且您可以在该语句之前放置一个case标签.到目前为止,我没有看到允许这一点,但在这种特殊情况下,将开关隐藏在一个漂亮的宏后面是很方便的.

  • 哦,聪明。您甚至可以将其隐藏在“定义块开关(0)情况0:”之类的宏后面,并像“块{... break; }`。 (2认同)

Dan*_*ard 15

我会推荐一种类似于Mats的方法,减去不必要的方法goto.只将条件逻辑放在函数中.任何始终运行的代码都应该在调用者调用函数之前或之后进行:

void main()
{
    //do stuff always
    func();
    //do other stuff always
}

void func()
{
    if (!condition)
        return;
    ...
    if (!other condition)
        return;
    ...
    if (!another condition)
        return;
    ... 
    if (!yet another condition)
        return;
    ...
}
Run Code Online (Sandbox Code Playgroud)

  • 如果你必须在`func`中间获取另一个资源,你需要分解另一个函数(根据你的模式).如果所有这些孤立的函数都需要相同的数据,那么你最终会反复复制相同的堆栈参数,或者决定在堆上分配你的参数并传递一个指针,而不是利用该语言最基本的功能(函数参数).简而言之,我不相信这种解决方案可以在最坏的情况下进行扩展,在这种情况下,在获取必须清理的新资源后检查每个条件.注意Nawaz对lambdas的评论. (3认同)
  • 我不记得OP在他的代码块中间说了解有关获取资源的任何内容,但我接受这是一个可行的要求.在那种情况下,在`func()`中的任何地方声明堆栈上的资源并允许其析构函数处理释放资源有什么问题?如果`func()`之外的任何东西需要访问相同的资源,它应该在由适当的资源管理器调用`func()`之前在堆上声明. (2认同)

ste*_*anv 12

代码流本身已经是代码气味,在函数中发生了很多.如果没有直接解决方案(该函数是一般检查函数),那么使用RAII,这样你可以返回而不是跳转到函数的结尾部分可能会更好.


ace*_*egs 12

对我do{...}while(0)来说很好.如果您不想看到do{...}while(0),可以为它们定义替代关键字.

例:

//--------SomeUtilities.hpp---------
#define BEGIN_TEST do{
#define END_TEST }while(0);

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) break;
   if(!condition2) break;
   if(!condition3) break;
   if(!condition4) break;
   if(!condition5) break;

   //processing code here

END_TEST
Run Code Online (Sandbox Code Playgroud)

我认为编译器将删除二进制版本中的不必要while(0)条件do{...}while(0),并将中断转换为无条件跳转.您可以检查它的汇编语言版本.

使用goto也可以产生更干净的代码,并且条件 - 然后 - 跳转逻辑很简单.您可以执行以下操作:

{
   if(!condition1) goto end_blahblah;
   if(!condition2) goto end_blahblah;
   if(!condition3) goto end_blahblah;
   if(!condition4) goto end_blahblah;
   if(!condition5) goto end_blahblah;

   //processing code here

 }end_blah_blah:;  //use appropriate label here to describe...
                   //  ...the whole code inside the block.
Run Code Online (Sandbox Code Playgroud)

请注意,标签是在结束后放置的}.这是避免一个可能的问题,goto因为你没有看到标签而意外地在其间放置代码.它现在就像do{...}while(0)没有条件代码.

为了使此代码更清晰,更易于理解,您可以这样做:

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);
   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);
   if(!condition5) FAILED(NormalizeData);

END_TEST(NormalizeData)
Run Code Online (Sandbox Code Playgroud)

这样,您可以执行嵌套块并指定要退出/跳出的位置.

//--------SomeUtilities.hpp---------
#define BEGIN_TEST {
#define END_TEST(_test_label_) }_test_label_:;
#define FAILED(_test_label_) goto _test_label_

//--------SomeSourceFile.cpp--------
BEGIN_TEST
   if(!condition1) FAILED(NormalizeData);
   if(!condition2) FAILED(NormalizeData);

   BEGIN_TEST
      if(!conditionAA) FAILED(DecryptBlah);
      if(!conditionBB) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionCC) FAILED(DecryptBlah);

      // --We can now decrypt and do other stuffs.

   END_TEST(DecryptBlah)

   if(!condition3) FAILED(NormalizeData);
   if(!condition4) FAILED(NormalizeData);

   // --other code here

   BEGIN_TEST
      if(!conditionA) FAILED(TrimSpaces);
      if(!conditionB) FAILED(TrimSpaces);
      if(!conditionC) FAILED(NormalizeData);   //Jump out to the outmost block
      if(!conditionD) FAILED(TrimSpaces);

      // --We can now trim completely or do other stuffs.

   END_TEST(TrimSpaces)

   // --Other code here...

   if(!condition5) FAILED(NormalizeData);

   //Ok, we got here. We can now process what we need to process.

END_TEST(NormalizeData)
Run Code Online (Sandbox Code Playgroud)

意粉代码不是故障goto,这是程序员的错.您仍然可以不使用而生成意大利面条代码goto.

  • 我选择`goto`来使用预处理器扩展语言语法一百万次. (9认同)
  • _“为了让这段代码更清晰、更易于理解,你可以[使用 LOADS_OF_WEIRD_MACROS]”_:不计算。 (2认同)

the*_*ill 11

如果您不需要在执行期间引入局部变量,那么您通常可以将其展平:

if (check()) {
  doStuff();
}  
if (stillOk()) {
  doMoreStuff();
}
if (amIStillReallyOk()) {
  doEvenMore();
}

// edit 
doThingsAtEndAndReportErrorStatus()
Run Code Online (Sandbox Code Playgroud)

  • 但是,每个条件都必须包含前一个条件,这不仅是丑陋的,而且可能对性能有害.一旦我们知道"不行",最好立即跳过这些支票并立即清理. (2认同)

Den*_*ore 10

与dasblinkenlight的答案类似,但避免了内部的分配if可能会被代码审查者"修复":

bool goOn = check0();
if (goOn) {
    ...
    goOn = check1();
}
if (goOn) {
    ...
    goOn = check2();
}
if (goOn) {
    ...
}
Run Code Online (Sandbox Code Playgroud)

...

我需要在下一步之前检查步骤的结果时使用此模式,这与所有检查都可以使用大型if( check1() && check2()...模式预先完成的情况不同.


Car*_*cho 10

使用例外.您的代码看起来会更清晰(并且为了处理程序执行流程中的错误而创建了异常).要清理资源(文件描述符,数据库连接等),请阅读文章为什么C++不提供"finally"结构?.

#include <iostream>
#include <stdexcept>   // For exception, runtime_error, out_of_range

int main () {
    try {
        if (!condition)
            throw std::runtime_error("nope.");
        ...
        if (!other condition)
            throw std::runtime_error("nope again.");
        ...
        if (!another condition)
            throw std::runtime_error("told you.");
        ...
        if (!yet another condition)
            throw std::runtime_error("OK, just forget it...");
    }
    catch (std::runtime_error &e) {
        std::cout << e.what() << std::endl;
    }
    catch (...) {
        std::cout << "Caught an unknown exception\n";
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

  • 真?首先,我真的没有看到任何可读性改进.不要使用例外来控制程序流.这不是他们的正当目的.此外,例外会带来重大的性能损失.异常的正确用例是当某些条件存在时您无法做任何事情,例如尝试打开已被其他进程专门锁定的文件,或者网络连接失败,或者对数据库的调用失败,或调用者将无效参数传递给过程.诸如此类的事情.但是,不要使用异常来控制程序流. (10认同)
  • 我的意思是,不要成为一个关于它的鼻涕,而是去你引用的Stroustrup文章,并寻找"我不应该使用什么例外?" 部分.除此之外,他说:_"特别是,throw不仅仅是从函数返回值的替代方法(类似于返回).这样做会很慢并且会混淆大多数用于查看仅用于的异常的C++程序员错误处理.同样,throw不是摆脱循环的好方法."_ (3认同)
  • @Craig所有你所指出的是对的,但你假设的示例程序可以后`检查()`条件未能继续下去,这就是你的certantly假设中,存在样品中没有上下文.假设程序无法继续,则使用异常是可行的方法. (3认同)
  • 好吧,如果这确实是上下文,那就足够真实了。但是,关于假设的有趣的事情... ;-) (2认同)

Ben*_*ict 8

从功能编程的角度来看,这是一个众所周知且解决得很好的问题 - 也许是monad.

为了回应我在下面收到的评论,我在这里编辑了我的介绍:你可以在各个地方找到有关实现C++ monad的全部细节,这将使你能够实现Rotsor的建议.grok monads需要一段时间,所以相反我会在这里建议一个快速的"穷人"monad-like机制,你只需要知道boost :: optional.

设置计算步骤如下:

boost::optional<EnabledContext> enabled(boost::optional<Context> context);
boost::optional<EnergisedContext> energised(boost::optional<EnabledContext> context);
Run Code Online (Sandbox Code Playgroud)

boost::none如果给出的可选项是空的,那么每个计算步骤显然都可以执行返回操作.例如:

struct Context { std::string coordinates_filename; /* ... */ };

struct EnabledContext { int x; int y; int z; /* ... */ };

boost::optional<EnabledContext> enabled(boost::optional<Context> c) {
   if (!c) return boost::none; // this line becomes implicit if going the whole hog with monads
   if (!exists((*c).coordinates_filename)) return boost::none; // return none when any error is encountered.
   EnabledContext ec;
   std::ifstream file_in((*c).coordinates_filename.c_str());
   file_in >> ec.x >> ec.y >> ec.z;
   return boost::optional<EnabledContext>(ec); // All ok. Return non-empty value.
}
Run Code Online (Sandbox Code Playgroud)

然后把它们连在一起:

Context context("planet_surface.txt", ...); // Close over all needed bits and pieces

boost::optional<EnergisedContext> result(energised(enabled(context)));
if (result) { // A single level "if" statement
    // do work on *result
} else {
    // error
}
Run Code Online (Sandbox Code Playgroud)

关于这一点的好处是你可以为每个计算步骤编写明确定义的单元测试.调用也像普通英语一样(通常是功能样式的情况).

如果你不关心不变性,每次使用shared_ptr等提出一些变化时返回相同的对象会更方便.

  • 该代码具有强制执行每个单独函数以处理先前函数的失败的不期望的属性,因此不能正确地利用Monad习语(其中monadic效应,在这种情况下失败,应该被隐式地处理).要做到这一点,你需要启用`optional <EnabledContext>(Context); 可选的<EnergisedContext>通电(EnabledContext);`而是使用monadic组合操作('bind')而不是函数应用程序. (3认同)

kar*_*erx 7

将if语句移动到一个产生数字或枚举结果的额外函数中怎么样?

int ConditionCode (void) {
   if (condition1)
      return 1;
   if (condition2)
      return 2;
   ...
   return 0;
}


void MyFunc (void) {
   switch (ConditionCode ()) {
      case 1:
         ...
         break;

      case 2:
         ...
         break;

      ...

      default:
         ...
         break;
   }
}
Run Code Online (Sandbox Code Playgroud)


Adr*_*hum 5

我并不特别喜欢使用breakreturn在这种情况下.鉴于通常在我们遇到这种情况时,通常是一种相对较长的方法.

如果我们有多个出口点,当我们想要知道什么会导致某些逻辑被执行时,它可能会造成困难:通常我们只是继续上升封闭那条逻辑的块,并且那些封闭块的标准告诉我们情况:

例如,

if (conditionA) {
    ....
    if (conditionB) {
        ....
        if (conditionC) {
            myLogic();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

通过查看封闭的块,很容易发现myLogic()只有在conditionA and conditionB and conditionC真实时才会发生.

当有早期回报时,它变得不那么明显:

if (conditionA) {
    ....
    if (!conditionB) {
        return;
    }
    if (!conditionD) {
        return;
    }
    if (conditionC) {
        myLogic();
    }
}
Run Code Online (Sandbox Code Playgroud)

我们再也无法向myLogic()上看,看着封闭的块来弄清楚条件.

我使用了不同的解决方法.这是其中之一:

if (conditionA) {
    isA = true;
    ....
}

if (isA && conditionB) {
    isB = true;
    ...
}

if (isB && conditionC) {
    isC = true;
    myLogic();
}
Run Code Online (Sandbox Code Playgroud)

(当然欢迎使用相同的变量来替换所有变量isA isB isC.)

这种方法至少会给读者提供代码,这myLogic()是在执行时执行的isB && conditionC.读者会得到一个提示,他需要进一步查找导致isB为真的内容.


lif*_*arn 5

或许这样的事情

#define EVER ;;

for(EVER)
{
    if(!check()) break;
}
Run Code Online (Sandbox Code Playgroud)

或使用例外

try
{
    for(;;)
        if(!check()) throw 1;
}
catch()
{
}
Run Code Online (Sandbox Code Playgroud)

使用例外,您也可以传递数据.

  • 请不要像你的定义那样做一些聪明的事情,它们通常会让代码更难以为同事们开发.我已经看到有人将Case定义为break;在一个头文件中使用case,并在cpp文件的一个开关中使用它,让其他人想知道为什么切换在Case语句之间断开了几个小时.哎呀... (10认同)
  • 当你命名宏时,你应该让它们看起来像宏(即全部大写).否则碰巧命名变量/函数/类型/等的人.名字`ever`将非常不开心...... (5认同)