有什么更好的方法可以避免do-while(0); 用C ++破解?


233

当代码流是这样的:

if(check())
{
  ...
  ...
  if(check())
  {
    ...
    ...
    if(check())
    {
      ...
      ...
    }
  }
}

我通常会看到此方法可以避免上述凌乱的代码流:

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

有什么更好的方法来避免这种变通方法/黑客行为,从而使其成为更高级别(行业级别)的代码?

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


38
RAII并引发异常。
ta.speot。是

135
对我来说,这似乎是一个使用的好时机goto-但我敢肯定有人会因为建议我而将我打败,所以我不会为此写答案。拥有长久do ... while(0);似乎是错误的事情。
Mats Petersson,

42
@dasblinkenlight:是的,的确如此。如果你要使用goto,要正视它,做它在开放,不要使用隐藏breakdo ... while
垫皮特森

44
@MatsPetersson:回答一下,我将为您的goto康复工作+1 。:)
wilx

27
@ ta.speot.is:“ RAII和引发异常”。这样,您将模拟带有异常的流控制。即有点像使用昂贵的尖端硬件作为锤子或镇纸。您可以这样做,但是对于我来说,那绝对是很不好的味道。
SigTerm

Answers:


309

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

例如:

void foo(...)
{
   if (!condition)
   {
      return;
   }
   ...
   if (!other condition)
   {
      return;
   }
   ...
   if (!another condition)
   {
      return;
   }
   ... 
   if (!yet another condition)
   {
      return;
   }
   ...
   // Some unconditional stuff       
}

22
@MatsPetersson:“在函数中隔离”意味着将重构为仅执行测试的新函数。
MSalters 2013年

35
+1。这也是一个很好的答案。在C ++ 11中,隔离函数可以是lambda,因为它也可以捕获局部变量,因此使事情变得容易!
纳瓦兹

4
@deworde:视情况而定,此解决方案可能会变得比goto更长,可读性更差。不幸的是,由于C ++不允许局部函数定义,因此您必须将该函数(将从中返回的函数)移到其他地方,这会降低可读性。它可能最终带有许多参数,然后,因为拥有许多参数是一件坏事,您将决定将它们包装到struct中,这将创建新的数据类型。在一个简单的情况下键入太多。
SigTerm

24
@Damon(如果有的话return)更干净,因为任何读者都会立即意识到它可以正常工作以及它在做什么。随着goto你必须环顾四周,看看它是什么,并确保没有失误。伪装好处。
R. Martinho Fernandes

11
@SigTerm:有时应将代码移至一个单独的函数,只是为了将每个函数的大小减小到易于理解的大小。如果您不想为函数调用付费,则将其标记为forceinline
Ben Voigt

257

有时候,使用goto实际上是正确的答案-至少对于那些没有因宗教信仰而长大的人来说,“goto无论问题是什么,永远都不可能成为答案”-这就是其中一种情况。

此代码仅将hack do { ... } while(0);用作将a装扮gotobreak。如果您要使用goto,请对此保持开放。使代码更难阅读没有意义。

一种特殊情况就是当您有很多代码且条件相当复杂时:

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

  }
  finish up. 
}

实际上,由于没有如此复杂的逻辑,因此可以使代码更清楚。

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

编辑:澄清一下,我绝不是建议将其goto用作一般解决方案。但是在某些情况下goto有比其他解决方案更好的解决方案。

想象一下,例如,我们正在收集一些数据,而要测试的不同条件是某种“这就是收集的数据的结束”,这取决于某种“继续/结束”标记,这些标记随位置而异您在数据流中。

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

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


76
不同意。goto可能有一个地方,但goto cleanup没有。清理是通过RAII完成的。
MSalters 2013年

14
@MSalters:假定清理涉及可以用RAII解决的问题。也许我应该说“问题错误”或类似的内容。
Mats Petersson

25
因此,对这一观点持goto否定态度,大概是出于宗教信仰,永远都不是正确的答案。如果有任何评论,我将不胜感激
Mats Petersson

19
人们讨厌,goto因为您必须考虑使用它/了解使用它的程序...另一方面,微处理器是建立在... 之上的jumpsconditional jumps所以问题出在某些人身上,而不是逻辑或其他方面。
woliveirajr

17
+1以适当使用goto。如果预期会出现错误(并非异常),则RAII 不是正确的解决方案,因为这会滥用异常。

82

您可以将简单的延续模式与bool变量一起使用:

bool goOn;
if ((goOn = check0())) {
    ...
}
if (goOn && (goOn = check1())) {
    ...
}
if (goOn && (goOn = check2())) {
    ...
}
if (goOn && (goOn = check3())) {
    ...
}

checkN返回时,该执行链将停止falsecheck...()由于&&操作员的短路,不会再执行其他呼叫。此外,优化的编译器是足够聪明,认识到设置goOnfalse是单行道,并插入缺少的goto end为您服务。结果,上面的代码的性能将与do/ 的性能相同while(0),而不会对其可读性造成痛苦的打击。


30
内部if条件分配看起来非常可疑。
米哈伊尔

90
@Mikhail只允许未经培训的眼睛。
dasblinkenlight

20
我用这个技术之前,它总是一种烦扰我,我想通了编译器必须生成代码在每个检查if不管是什么goOn,即使早期的一个失败(而不是跳跃/打破)是...但是我只是运行了一个测试,VS2012至少足够聪明,无论如何在发生第一个错误之后,所有东西都会短路。我会更经常使用它。 注:如果您使用的goOn &= checkN()checkN()永远即使运行goOnfalse在所涉及的起动if(即,不这样做)。
2013年

11
@Nawaz:如果您没有训练有素的思想在整个代码库中进行任意更改,那么与仅在ifs 内部进行分配相比,您将面临更大的问题。
Idelic

6
@sisharp优雅在情人眼中如此!我不明白世界上如何以某种方式将循环构造误用为“优雅的”,但是也许就是我。
dasblinkenlight

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

  2. 如果它与周围的代码过于紧密地耦合而无法执行此操作,并且您找不到减少耦合的方法,请在此代码块之后查看代码。据推测,它清除了该函数使用的一些资源。尝试使用RAII对象管理这些资源;然后将每个狡猾的对象替换breakreturn(或throw,如果更合适的话),然后让对象的析构函数为您清除。

  3. 如果程序流程过于混乱(以至于您确实需要)goto,请使用而不是给它一个奇怪的掩饰。

  4. 如果您有盲目禁止的编码规则goto,并且您确实无法简化程序流程,那么您可能不得不用do黑客来掩饰它。


4
我谦虚地认为RAI​​I虽然有用,但不是灵丹妙药。当您发现自己要编写一个没有其他用途的convert-goto-to-RAII类时,我肯定会认为,只要使用已经提到的“世界末日通行”成语,您就会得到更好的服务。
idoby

1
@busy_wait:确实,RAII不能解决所有问题;这就是为什么我的回答不止于第二点,而是继续建议goto这是否真的是一个更好的选择。
Mike Seymour,

3
我同意,但是我认为编写goto-RAII转换类是一个坏主意,我认为应该明确声明。
idoby

@idoby如何编写一个 RAII模板类,并在lambda中使用您需要的任何一次性使用清理实例化它
Caleth

@Caleth对我来说听起来像是您在重塑ctor / dtor?
idoby

37

TLDRRAII,事务代码(仅设置结果或在已经计算出结果时返回内容)和异常。

长答案:

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

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

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

这是最佳做法,因为:

  • 它是显式的(也就是说,虽然错误处理不是显式的,但是算法的主要流程是)

  • 编写客户端代码很简单

  • 最小

  • 很简单

  • 它没有重复的代码构造

  • 它不使用宏

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

  • 它是可以重用的,只需花费很少的精力(也就是说,如果我想将调用复制computation2();到另一个函数,则不必确保do { ... } while(0)在新代码中添加,也不必确保添加#definegoto包装宏和清理标签,也不必还要别的吗)。


+1。这是我尝试使用的方法,使用RAII或其他方法。shared_ptr使用自定义删除器可以做很多事情。在C ++ 11中使用lambda甚至更容易。
Macke

是的,但是如果您最终将shared_ptr用于不是指针的内容,请至少考虑对其进行类型定义:namespace xyz { typedef shared_ptr<some_handle> shared_handle; shared_handle make_shared_handle(a, b, c); };在这种情况下(make_handle在构造上设置正确的Deleter类型),该类型的名称不再表明它是指针。
utnapistim

21

为了完整起见,我添加一个答案。许多其他答案指出,大条件块可以拆分为单独的函数。但是,正如多次指出的那样,这种方法将条件代码与原始上下文分开。这是lambda被添加到C ++ 11语言的原因之一。其他人建议使用lambda,但未提供明确的示例。我在这个答案中放了一个。让我印象深刻的是,它do { } while(0)在很多方面都与这种方法非常相似-也许这意味着它仍然是goto变相...。

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

    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
}();
later operations

7
对我来说,这种骇客看上去比做...骇客更糟。
迈克尔,

18

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

代替 :

do {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

您可以这样写:

switch (0) {
case 0:
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

这仍然是变相的goto,但是至少它不再是循环。这意味着您将不必非常仔细地检查是否没有继续块中隐藏的内容。

构造也很简单,您可以希望编译器对其进行优化。

正如@jamesdlin所建议的那样,您甚至可以将其隐藏在像

#define BLOC switch(0) case 0:

并像这样使用

BLOC {
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
}

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


2
哦,聪明。您甚至可以将其隐藏在macro之后define BLOCK switch (0) case 0:,然后像like 那样使用BLOCK { ... break; }
jamesdlin

@jamesdlin:我从未想过,在将箱子放在开关的开口支架之前可能很有用。但是C确实允许它,在这种情况下,编写一个不错的宏很方便。
克里斯,

15

我建议一种类似于Mats answer减去不必要的方法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;
    ...
}

3
如果您必须在的中间获取其他资源,则func需要根据您的模式分解出另一个函数。如果所有这些隔离的函数都需要相同的数据,那么您最终将一遍又一遍地复制相同的堆栈参数,或者决定在堆上分配参数并传递一个指针,而不会利用该语言最基本的功能(函数参数)。简而言之,我不认为此解决方案适用于最坏的情况,在这种情况下,在获取必须清除的新资源后会检查所有条件。请注意,Nawaz关于lambda的评论。
TNE

2
我不记得OP在他的代码块中间说过关于获取资源的任何事情,但是我接受这是一个可行的要求。在那种情况下,在堆栈中任何地方声明资源func()并允许其析构函数处理释放资源有什么问题?如果外部func()需要访问同一资源,则应在func()由适当的资源管理器调用之前在堆上声明它。
丹·贝查德

12

代码流本身已经是函数中经常发生的代码异味。如果没有直接解决方案(该函数是常规检查函数),请使用RAII,以便您可以返回而不是跳转到该函数的结尾部分,可能会更好。


11

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

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

// edit 
doThingsAtEndAndReportErrorStatus()

2
但是,每个条件都必须包括前一个条件,这不仅很丑陋,而且可能会降低性能。最好在我们知道我们“状况不佳”后立即跳过这些检查并进行清理。
tne 2013年

没错,但是如果必须在最后完成某些事情,那么您就不想再早回来了(请参见编辑),这种方法确实具有优势。如果您结合Denise在这种情况下使用bool的方法,那么除非这是一个非常紧密的循环,否则性能影响将是微不足道的。
the_mandrill

10

与dasblinkenlight的答案类似,但避免if了代码审阅者可能“修复”的内部分配:

bool goOn = check0();
if (goOn) {
    ...
    goOn = check1();
}
if (goOn) {
    ...
    goOn = check2();
}
if (goOn) {
    ...
}

...

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


注意,check1()实际上可以是PerformStep1(),它返回步骤的结果代码。这将降低流程流功能的复杂性。
丹妮丝·斯基德莫尔

10

使用例外。您的代码看起来更加简洁(并且为处理程序执行流程中的错误而专门创建了异常)。为了清理资源(文件描述符,数据库连接等),请阅读文章为什么C ++不提供“最终”构造?

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

10
真?首先,老实说,我看不到任何可读性的改进。请勿使用例外来控制程序流程。那不是他们的正确目的。同样,例外会带来重大的性能损失。出现异常的正确用例是,在某些情况下您无能为力,例如尝试打开已经由另一个进程独占锁定的文件,网络连接失败或对数据库的调用失败,或调用者将无效参数传递给过程。诸如此类的事情。但是请勿使用异常来控制程序流。
克雷格(Craig)

3
我的意思是,不要be之以鼻,而是转到您引用的Stroustrup文章,然后查找“我不应该对异常使用什么?” 部分。他说: “特别是,throw不仅是从函数返回值的另一种方法(类似于return)。这样做会很慢,并且会使大多数习惯于仅用于错误的异常的C ++程序员感到困惑。同样,抛出不是摆脱循环的好方法。”
克雷格2013年

3
@Craig您所指出的一切都是正确的,但是您假设示例程序可以在check()条件失败后继续运行,并且这确实是您的假设,示例中没有上下文。假定程序无法继续,使用异常是解决方法。
Cartucho

2
好吧,如果这确实是上下文,那就足够真实了。但是,关于假设的有趣事情... ;-)
Craig

10

对我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

我认为编译器将删除不必要的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.

请注意,标签位于结束符之后}。这是可以避免的一个可能的问题,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)

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

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

Spaghetti代码不是的错goto,而是程序员的错。您仍然可以在不使用的情况下生成意大利面条代码goto


10
我选择goto使用预处理器将语言语法扩展一百万次。
基督教徒

2
“为了使此代码更清晰,更易于理解,您可以[使用LOADS_OF_WEIRD_MACROS]”:不计算。
underscore_d

8

从功能编程的角度来看,这是一个众所周知且已解决的问题-也许是单子。

为了回应我在下面收到的评论,我在这里编辑了我的介绍:您可以在各个地方找到有关实现C ++ monad的完整详细信息,这将使您实现Rotsor的建议。弄混monad会花费一些时间,因此我将在这里建议一种快速的“穷人” monad式机制,您只需要boost :: optional就可以了。

设置计算步骤如下:

boost::optional<EnabledContext> enabled(boost::optional<Context> context);
boost::optional<EnergisedContext> energised(boost::optional<EnabledContext> context);

boost::none如果给定的可选步骤为空,则每个计算步骤显然都可以执行类似return 的操作。因此,例如:

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

然后将它们链接在一起:

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
}

这样做的好处是,您可以为每个计算步骤编写明确定义的单元测试。调用也读起来像普通的英语(通常是函数样式的情况)。

如果您不关心不变性,那么每次使用shared_ptr或类似的方法提出一些变化时,返回相同的对象会更方便。


3
此代码具有强制每个单独的函数处理先前功能的故障的不良特性,因此无法正确利用Monad惯用语(在这种情况下,应以隐式方式处理Monadic效果,在这种情况下为失败)。为此,您需要optional<EnabledContext> enabled(Context); optional<EnergisedContext> energised(EnabledContext);使用monadic合成操作(“ bind”)而不是函数应用程序。
Rotsor

谢谢。您是正确的-这是正确执行此操作的方法。我不想在答案中写太多来解释这一点(因此,“穷人”一词的意思是暗示我不会在这里大吃大喝)。
本尼迪克特

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

如果可能的话,这很好,但是比这里提出的问题要笼统得多。每个条件都可能取决于上次脱支测试后执行的代码。
克里斯,

这里的问题是您将因果分开。也就是说,您单独的代码引用了相同的条件编号,这可能会导致其他错误。
里加

@kriss:好吧,可以调整ConditionCode()函数来解决这个问题。它们的关键点是,一旦计算出最终条件,就可以使用return <result>进行干净退出。这就是在此提供结构清晰性的原因。
karx11erx

@里加:Imo这些完全是学术上的反对。我发现C ++在每个新版本中都变得更加复杂,含糊且难以理解。我看不到一个小的辅助函数以结构良好的方式评估复杂条件以使下面的目标函数更具可读性的问题。
karx11erx

1
@ karx11erx我的反对是切合实际的,并且基于我的经验。这种模式与C ++ 11或其他语言无关。如果您在使用语言构造方面遇到困难,可以使您编写出良好的体系结构,这不是语言问题。
里加2013年

5

像这样的东西

#define EVER ;;

for(EVER)
{
    if(!check()) break;
}

或使用例外

try
{
    for(;;)
        if(!check()) throw 1;
}
catch()
{
}

使用异常,您还可以传递数据。


10
请不要做像您永远这样的定义之类的聪明事,它们通常会使其他开发人员难以阅读代码。我见过有人将Case定义为break; case在头文件中,并在cpp文件中的开关中使用它,使其他人想知道为什么开关在Case语句之间中断。Grrr ...
Michael

5
当您命名宏时,应该使它们看起来像宏(即全部大写)。否则,碰巧会命名变量/函数/类型/等的人。命名ever将非常不开心...
jamesdlin

5

我并不特别喜欢使用 breakreturn在这种情况下。考虑到通常情况下,当我们面对这种情况时,这通常是一个比较长的方法。

如果我们有多个出口点,则当我们想知道什么将导致执行某些逻辑时,可能会造成困难:通常,我们只是继续向上执行包含该逻辑块的代码块,而这些代码块的标准告诉我们情况:

例如,

if (conditionA) {
    ....
    if (conditionB) {
        ....
        if (conditionC) {
            myLogic();
        }
    }
}

通过查看封闭的块,很容易发现myLogic()只有在conditionA and conditionB and conditionCtrue 时才发生。

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

if (conditionA) {
    ....
    if (!conditionB) {
        return;
    }
    if (!conditionD) {
        return;
    }
    if (conditionC) {
        myLogic();
    }
}

我们不能再从上浏览myLogic(),而是查看封闭的框来找出情况。

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

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

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

if (isB && conditionC) {
    isC = true;
    myLogic();
}

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

这种方法将至少为读者提供代码,该代码在以下myLogic()情况下执行isB && conditionC。提示读者他需要进一步查找将导致isB为真的内容。


3
typedef bool (*Checker)();

Checker * checkers[]={
 &checker0,&checker1,.....,&checkerN,NULL
};

bool checker1(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

bool checker2(){
  if(condition){
    .....
    .....
    return true;
  }
  return false;
}

......

void doCheck(){
  Checker ** checker = checkers;
  while( *checker && (*checker)())
    checker++;
}

那个怎么样?


如果过时了,只是return condition;,否则我认为这是可以很好维护的。
SpaceTrucker

2

如果需要根据失败的位置采取不同的清理步骤,则另一种模式很有用:

    private ResultCode DoEverything()
    {
        ResultCode processResult = ResultCode.FAILURE;
        if (DoStep1() != ResultCode.SUCCESSFUL)
        {
            Step1FailureCleanup();
        }
        else if (DoStep2() != ResultCode.SUCCESSFUL)
        {
            Step2FailureCleanup();
            processResult = ResultCode.SPECIFIC_FAILURE;
        }
        else if (DoStep3() != ResultCode.SUCCESSFUL)
        {
            Step3FailureCleanup();
        }
        ...
        else
        {
            processResult = ResultCode.SUCCESSFUL;
        }
        return processResult;
    }

2

我不是C ++程序员,所以在这里我不会写任何代码,但是到目前为止,没有人提到面向对象的解决方案。所以这是我的猜测:

具有通用接口,该接口提供一种评估单个条件的方法。现在,您可以在包含相关方法的对象中使用这些条件的实现列表。您遍历该列表并评估每种条件,如果一个条件失败,可能会提早爆发。

好的是,这样的设计很好地遵循开/关原理,因为您可以在包含所讨论方法的对象初始化期间轻松添加新条件。您甚至可以在接口中添加第二种方法,其中条件评估方法将返回条件描述。这可以用于自文档系统。

但是,缺点是,由于使用了更多的对象并且遍历了列表,因此所涉及的开销稍微增加了。


您可以添加其他语言的示例吗?我认为这个问题适用于许多语言,尽管它是专门针对C ++提出的。
Denise Skidmore

1

这就是我这样做的方式。

void func() {
  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...

  if (!check()) return;
  ...
  ...
}

1

首先,通过一个简短的示例来说明为什么gotoC ++不是一个好的解决方案:

struct Bar {
    Bar();
};

extern bool check();

void foo()
{
    if (!check())
       goto out;

    Bar x;

    out:
}

尝试将其编译为目标文件,然后看看会发生什么。然后尝试等效的do+ break+while(0)

放在一边。要点如下。

如果整个函数失败,这些小小的代码块通常需要某种清理。当您“展开”部分完成的计算时,这些清理通常希望以与块本身相反的顺序进行。

获得这些语义的一种选择是RAII ; 参见@utnapistim的答案。C ++保证自动析构函数以与构造函数相反的顺序运行,自然会提供“解散”。

但这需要很多RAII类。有时一个更简单的选择就是使用堆栈:

bool calc1()
{
    if (!check())
        return false;

    // ... Do stuff1 here ...

    if (!calc2()) {
        // ... Undo stuff1 here ...
        return false;
    }

    return true;
}

bool calc2()
{
    if (!check())
        return false;

    // ... Do stuff2 here ...

    if (!calc3()) {
        // ... Undo stuff2 here ...
        return false;
    }

    return true;
}

...等等。这很容易审核,因为它会将“撤消”代码放在“执行”代码旁边。简单的审核是好的。这也使控制流程非常清晰。对于C来说,这也是一个有用的模式。

它可能要求calc函数接受大量参数,但是如果您的类/结构具有良好的内聚性,通常这不是问题。(也就是说,属于同一个对象的东西生活在一个对象中,因此这些函数可以使用指向少量对象的指针或引用,但仍然可以做很多有用的工作。)


审核清理路径非常容易,但跟踪黄金路径可能并不那么容易。但是总的来说,我认为类似的事情确实鼓励了一致的清理模式。
丹妮丝·斯基德莫尔

0

如果您的代码中有很长的if..else if..else语句块,则可以尝试借助Functors或重写整个块function pointers。不一定总是正确的解决方案,但通常是。

http://www.cprogramming.com/tutorial/functors-function-objects-in-c++.html


原则上,这是可能的(并且不错),但是使用显式的函数对象或-pointer只会严重破坏代码流。OTOH,在这里等同于使用lambda或普通的命名函数,是一种很好的实践,高效且读取良好。
左转

0

我对这里提出的不同答案感到惊讶。但是,最后,在我必须更改的代码(即删除此do-while(0)hack或其他内容)中,我做了一些不同于此处提到的任何答案的事情,我感到困惑,为什么没人想到这一点。这是我所做的:

初始代码:

do {

    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
    if(!check()) break;
    ...
    ...
} while(0);

finishingUpStuff.

现在:

finish(params)
{
  ...
  ...
}

if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...
if(!check()){
    finish(params);    
    return;
}
...
...

因此,这里所做的是将整理工作隔离在一个函数中,并且事情突然变得如此简单和干净!

我认为该解决方案值得一提,因此请在此处提供。


0

将其合并为一个if语句:

if(
    condition
    && other_condition
    && another_condition
    && yet_another_condition
    && ...
) {
        if (final_cond){
            //Do stuff
        } else {
            //Do other stuff
        }
}

这是在Java之类的语言中使用的模式,其中删除了goto关键字。


2
仅当您在条件测试之间不必做任何事情时,这才起作用。(好吧,我想您可以将条件做隐藏在条件测试所进行的某些函数调用中,但是如果您做得太多,可能会感到有些困惑)
Jeremy Friesner 2013年

@JeremyFriesner实际上,实际上,您可以将中间的东西作为单独的布尔函数执行,始终将其评估为true。短路评估将确保您不会在所有前提测试未通过的测试之间进行测试。
AJMansfield

@AJMansfield是的,这就是我在第二句话中提到的内容……但是我不确定这样做会提高代码质量。
杰里米·弗里斯纳

@JeremyFriesner没有什么可以阻止您将条件写为(/*do other stuff*/, /*next condition*/),甚至可以很好地格式化它。只是不要期望人们喜欢它。但老实说,这只能表明Java淘汰该goto声明是错误的……
cmaster-恢复monica

@JeremyFriesner我以为这些是布尔值。如果要在每个条件内运行函数,则有一种更好的方法来处理它。
Tyzoid

0

如果对所有错误使用相同的错误处理程序,并且每个步骤都返回一个布尔值,表示成功:

if(
    DoSomething() &&
    DoSomethingElse() &&
    DoAThirdThing() )
{
    // do good condition action
}
else
{
    // handle error
}

(类似于tyzoid的回答,但条件是动作,&&防止第一次失败后再发生其他动作。)


0

为什么没有标记方法回答了很久以来就使用过。

//you can use something like this (pseudocode)
long var = 0;
if(condition)  flag a bit in var
if(condition)  flag another bit in var
if(condition)  flag another bit in var
............
if(var == certain number) {
Do the required task
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.