234

当代码流是这样的时候:

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

我通常已经看到了这种解决方法,以避免上述混乱的代码流:

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

有哪些更好的方法可以避免这种变通/破解,使其成为更高级别(行业级别)的代码?

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

4

27 回答 27

312

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

例如:

void foo(...)
{
   if (!condition)
   {
      return;
   }
   ...
   if (!other condition)
   {
      return;
   }
   ...
   if (!another condition)
   {
      return;
   }
   ... 
   if (!yet another condition)
   {
      return;
   }
   ...
   // Some unconditional stuff       
}
于 2013-08-29T09:53:17.737 回答
260

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

此代码使用 hackdo { ... } 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解决方案比其他解决方案更好。

例如,假设我们正在收集一些数据,并且正在测试的不同条件是某种“这是正在收集的数据的结束” - 这取决于某种“继续/结束”标记,这些标记因位置而异你在数据流中。

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

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

于 2013-08-29T10:05:41.363 回答
82

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

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

此执行链将在checkN返回 a时立即停止false。由于接线员check...()的短路,不会再进行呼叫。&&此外,优化编译器足够聪明,可以识别设置goOnfalse单向街道,并goto end为您插入缺失的内容。结果,上述代码的性能将与do/的性能相同while(0),只是对其可读性没有严重影响。

于 2013-08-29T10:14:12.237 回答
38
  1. 尝试将代码提取到一个单独的函数中(或者可能不止一个)。如果检查失败,则从函数返回。

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

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

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

于 2013-08-29T10:01:24.813 回答
37

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

长答案:

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

在 C 中,在大多数代码库中,if(error_ok != ...andgoto代码通常隐藏在一些便利宏(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)在新代码中添加 a ,也不必确保#definegoto 包装器宏和清理标签,也还要别的吗)。

于 2013-08-29T10:52:58.320 回答
21

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

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

    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
    if (!check()) return;
    ...
    ...
}();
later operations
于 2013-08-29T17:52:38.147 回答
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 语言语法期望在 switch 之后有一个语句,而不是括号中的块,并且您可以在该语句之前放置一个 case 标签。到目前为止,我还没有看到允许这样做的意义,但在这种特殊情况下,将开关隐藏在一个不错的宏后面是很方便的。

于 2013-08-29T22:55:34.793 回答
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;
    ...
}
于 2013-08-29T14:06:10.860 回答
12

代码流本身已经是一种代码味道,在函数中发生了很多事情。如果没有直接的解决方案(该函数是通用检查函数),那么使用RAII以便您可以返回而不是跳转到函数的末尾部分可能会更好。

于 2013-08-29T10:02:17.050 回答
11

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

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

// edit 
doThingsAtEndAndReportErrorStatus()
于 2013-08-29T09:55:39.683 回答
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)

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

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)

意大利面条代码不是程序员的错goto,而是程序员的错。您仍然可以在不使用goto.

于 2013-08-30T03:38:19.693 回答
10

类似于 dasblinkenlight 的答案,但避免if了代码审查员可能会“修复”的内部分配:

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

...

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

于 2013-08-29T16:35:56.477 回答
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;
}
于 2013-08-29T19:03:52.623 回答
8

从函数式编程的角度来看,这是一个众所周知且得到很好解决的问题——maybe monad。

为了回应我在下面收到的评论,我在这里编辑了我的介绍:您可以在各个地方找到有关实现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 等提出一些变化。

于 2013-08-29T13:57:18.433 回答
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;
   }
}
于 2013-08-29T14:04:03.697 回答
5

可能是这样的

#define EVER ;;

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

或使用例外

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

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

于 2013-08-30T08:02:40.453 回答
5

我不是特别喜欢使用breakreturn在这种情况下的方式。鉴于通常当我们面临这种情况时,它通常是一个比较长的方法。

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

例如,

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

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

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

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 为真的原因。

于 2013-08-30T04:17:32.090 回答
3
typedef bool (*Checker)();

Checker * checkers[]={
 &checker0,&checker1,.....,&amp;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++;
}

那个怎么样?

于 2013-09-04T16:58:07.880 回答
2

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

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

好消息是这样的设计非常符合开闭原则,因为您可以在包含相关方法的对象的初始化过程中轻松添加新条件。您甚至可以使用返回条件描述的条件评估方法向接口添加第二个方法。这可用于自记录系统。

然而,不利的一面是,由于使用了更多对象和对列表的迭代,因此涉及的开销稍大一些。

于 2013-09-05T06:38:17.943 回答
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;
    }
于 2013-09-05T14:03:59.600 回答
1

我就是这样做的。

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

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

  if (!check()) return;
  ...
  ...
}
于 2013-09-11T02:18:33.777 回答
1

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

struct Bar {
    Bar();
};

extern bool check();

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

    Bar x;

    out:
}

试着把它编译成一个目标文件,看看会发生什么。然后尝试等效的do++ 。breakwhile(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函数接受大量参数,但如果您的类/结构具有良好的内聚性,这通常不是问题。(也就是说,属于一起的东西存在于单个对象中,因此这些函数可以获取少量对象的指针或引用,并且仍然可以做很多有用的工作。)

于 2013-09-05T02:56:00.517 回答
0

我对这里提供的不同答案的数量感到惊讶。但是,最后在我必须更改的代码中(即删除这个do-while(0)黑客​​或任何东西),我做了一些与这里提到的任何答案不同的事情,我很困惑为什么没有人想到这一点。这是我所做的:

初始代码:

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

所以,这里所做的就是将整理的东西隔离在一个函数中,事情突然变得如此简单和干净!

我认为这个解决方案值得一提,所以在这里提供。

于 2013-09-28T12:18:32.123 回答
0

将其合并为一个if语句:

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

这是在 Java 等语言中使用的模式,其中 goto 关键字已被删除。

于 2013-08-29T14:55:54.690 回答
0

Functors如果你的代码有很长的 if..else if..else 语句块,你可以尝试在or的帮助下重写整个块function pointers。它可能并不总是正确的解决方案,但通常是正确的。

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

于 2013-08-29T09:57:26.130 回答
0

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

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

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

于 2015-02-26T00:20:43.890 回答
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
}
于 2016-02-19T02:10:16.893 回答