使用'goto'控制流的宏

时间:2009-03-31 08:55:47

标签: c error-handling coding-style goto flow-control

是的,两个讨厌的结构合并。它听起来是不是很糟糕,还是被视为控制goto使用的好方法,也提供了合理的清理策略?

在工作中,我们讨论了是否允许在我们的编码标准中使用goto。一般来说,没有人想允许免费使用goto,但有些人对使用它进行清理跳转是积极的。如在此代码中:

void func()
{
   char* p1 = malloc(16);
   if( !p1 )
      goto cleanup;

   char* p2 = malloc(16);
   if( !p2 )
      goto cleanup;

 goto norm_cleanup;

 err_cleanup:

   if( p1 )
      free(p1);

   if( p2 )
      free(p2);

 norm_cleanup:
}

此类使用的巨大好处是您不必最终得到此代码:

void func()
{
   char* p1 = malloc(16);
   if( !p1 ){
      return;
   }

   char* p2 = malloc(16);
   if( !p2 ){
      free(p1);
      return;
   }

   char* p3 = malloc(16);
   if( !p3 ){
      free(p1);
      free(p2);
      return;
   }
}

特别是在具有许多分配的类似构造函数的函数中,这有时会变得非常糟糕,尤其是当有人必须在中间插入某些内容时。

因此,为了能够使用goto,但仍然明确地将其与自由使用隔离,创建了一组流控制宏来处理任务。看起来像这样(简化):

#define FAIL_SECTION_BEGIN int exit_code[GUID] = 0;
#define FAIL_SECTION_DO_EXIT_IF( cond, exitcode ) if(cond){exit_code[GUID] = exitcode; goto exit_label[GUID];}
#define FAIL_SECTION_ERROR_EXIT(code) exit_label[GUID]: if(exit_code[GUID]) int code = exit_code[GUID];else goto end_label[GUID]
#define FAIL_SECTION_END end_label[GUID]:

我们可以使用如下:

int func()
{
   char* p1 = NULL;
   char* p2 = NULL;
   char* p3 = NULL;

   FAIL_SECTION_BEGIN
   {
      p1 = malloc(16);
      FAIL_SECTION_DO_EXIT_IF( !p1, -1 );

      p2 = malloc(16);
      FAIL_SECTION_DO_EXIT_IF( !p2, -1 );

      p3 = malloc(16);
      FAIL_SECTION_DO_EXIT_IF( !p3, -1 );
   }
   FAIL_SECTION_ERROR_EXIT( code )
   {
      if( p3 ) 
         free(p3);

      if( p2 ) 
         free(p2);

      if( p1 ) 
         free(p1);

      return code;
    }
    FAIL_SECTION_END

  return 0;

它看起来不错,并带来许多好处,但是,在将其推广到开发之前,是否有任何我们应该考虑的缺点?毕竟非常流量控制和转到:ish。两人都气馁。在这种情况下,阻止他们的理由是什么?

感谢。

9 个答案:

答案 0 :(得分:11)

错误处理是goto不是那么糟糕的罕见情况之一。

但如果我必须维护该代码,我会非常沮丧goto被宏隐藏。

所以在这种情况下goto对我来说没问题,但不是宏。

答案 1 :(得分:7)

使用goto转到常见的错误处理程序/清理/退出序列是绝对正常的。

答案 2 :(得分:7)

此代码:

void func()
{
   char* p1 = malloc(16);
   if( !p1 )
      goto cleanup;

   char* p2 = malloc(16);
   if( !p2 )
      goto cleanup;

 cleanup:

   if( p1 )
      free(p1);

   if( p2 )
      free(p2);
}

可以合法地写成:

void func()
{
   char* p1 = malloc(16);
   char* p2 = malloc(16);

    free(p1);
    free(p2);
}

内存分配是否成功。

这是有效的,因为如果传递NULL指针,free()不会执行任何操作。在设计自己的API以分配和释放其他资源时,您可以使用相同的习惯用法:

// return handle to new Foo resource, or 0 if allocation failed
FOO_HANDLE AllocFoo();

// release Foo indicated by handle, - do nothing if handle is 0
void ReleaseFoo( FOO_HANDLE h );

设计这样的API可以大大简化资源管理。

答案 3 :(得分:3)

使用goto进行清理是一种常见的C惯用法,used in Linux kernel *。

**也许Linus的观点不是一个好的论证的最佳例子,但它确实显示goto被用于一个相对大规模的项目。*

答案 4 :(得分:3)

如果第一个malloc失败,那么清理p1和p2。由于goto,p2未初始化并且可能指向任何内容。我用gcc快速运行它来检查并尝试释放(p2)确实会导致seg故障。

在上一个示例中,变量的作用域在大括号内(即它们只存在于FAIL_SECTION_BEGIN块中)。

假设代码在没有大括号的情况下工作,你仍然需要在FAIL_SECTION_BEGIN之前将所有指针初始化为NULL,以避免出现断层错误。

我没有反对goto和宏,但我更喜欢Neil Butterworth的想法......

void func(void)
{
    void *p1 = malloc(16);
    void *p2 = malloc(16);
    void *p3 = malloc(16);

    if (!p1 || !p2 || !p3) goto cleanup;

    /* ... */

cleanup:
    if (p1) free(p1);
    if (p2) free(p2);
    if (p3) free(p3);
}

或者如果它更合适..

void func(void)
{
    void *p1 = NULL;
    void *p2 = NULL;
    void *p3 = NULL;

    p1 = malloc(16);
    if (!p1) goto cleanup;

    p2 = malloc(16);
    if (!p2) goto cleanup;

    p3 = malloc(16);
    if (!p3) goto cleanup;

    /* ... */

cleanup:
    if (p1) free(p1);
    if (p2) free(p2);
    if (p3) free(p3);
}

答案 5 :(得分:2)

术语“结构化编程”我们都知道它是反goto的东西最初是作为一堆编码模式与goto(或JMP)开发和开发的。这些模式被称为whileif模式等。

因此,如果您使用goto,请以结构化方式使用它们。这限制了损害。而这些宏似乎是一种合理的方法。

答案 6 :(得分:2)

原始代码将受益于使用多个return语句 - 无需跳转错误返回清理代码。另外,您通常也需要在普通回程中释放分配的空间 - 否则您会泄漏内存。如果你小心的话,你可以在没有goto的情况下重写这个例子。在这种情况下,您可以在必要时有用地声明变量:

void func()
{
    char *p1 = 0;
    char *p2 = 0;
    char *p3 = 0;

    if ((p1 = malloc(16)) != 0 &&
        (p2 = malloc(16)) != 0 &&
        (p3 = malloc(16)) != 0)
    {
        // Use p1, p2, p3 ...
    }
    free(p1);
    free(p2);
    free(p3);
}

如果在每次分配操作后都有非常重要的工作量,那么您可以在free()操作的第一个之前使用标签,并且goto是正常的 - 错误处理是主要的这些天使用goto的原因,以及其他任何事情都有点可疑。

我会查看一些具有嵌入式goto语句的宏的代码。第一次遇到令人困惑的是看到可见代码“未引用”的标签,但无法删除。我宁愿避免这种做法。当我不需要知道他们做什么时,宏就可以了 - 他们就是这么做的。当您必须知道它们扩展到什么以准确使用它们时,宏不是那么好。如果他们不向我隐瞒信息,那么他们更多的是麻烦而不是帮助。

插图 - 伪装以保护有罪的名字:

#define rerrcheck if (currval != &localval && globvar->currtub &&          \
                    globvar->currtub->te_flags & TE_ABORT)                 \
                    { if (globvar->currtub->te_state)                      \
                         globvar->currtub->te_state->ts_flags |= TS_FAILED;\
                      else                                                 \
                         delete_tub_name(globvar->currtub->te_name);       \
                      goto failure;                                        \
                    }


#define rgetunsigned(b) {if (_iincnt>=2)  \
                           {_iinptr+=2;_iincnt-=2;b = ldunsigned(_iinptr-2);} \
                         else {b = _igetunsigned(); rerrcheck}}

rgetunsigned()上有几十个变体有些相似 - 不同大小和不同的加载器功能。

使用这些循环的一个地方包含这个循环 - 在一个大型交换机的单个案例中的一个更大的代码块中,有一些小的和一些大的代码块(结构不是特别好):

        for (i = 0 ; i < no_of_rows; i++)
            {
            row_t *tmprow = &val->v_coll.cl_typeinfo->clt_rows[i];

            rgetint(tmprow->seqno);
            rgetint(tmprow->level_no);
            rgetint(tmprow->parent_no);
            rgetint(tmprow->fieldnmlen);
            rgetpbuf(tmprow->fieldname, IDENTSIZE);
            rgetint(tmprow->field_no);
            rgetint(tmprow->type);
            rgetint(tmprow->length);
            rgetlong(tmprow->xid);
            rgetint(tmprow->flags);
            rgetint(tmprow->xtype_nm_len);
            rgetpbuf(tmprow->xtype_name, IDENTSIZE);
            rgetint(tmprow->xtype_owner_len);
            rgetpbuf(tmprow->xtype_owner_name, IDENTSIZE);
            rgetpbuf(tmprow->xtype_owner_name,
                     tmprow->xtype_owner_len);
            rgetint(tmprow->alignment);
            rgetlong(tmprow->sourcetype);
            }

那里的代码与goto语句相关并不明显!显而易见,对它所来自的代码的完全解释将需要一整天 - 它们是多种多样的。

答案 7 :(得分:1)

第一个例子看起来比宏观版本更具可读性。 而且mouviciel说得比我做得好得多

答案 8 :(得分:0)

#define malloc_or_die(size) if(malloc(size) == NULL) exit(1)

除非您拥有值得编写交易系统的软件,否则您无法真正从失败的malloc中恢复,如果您这样做,则将回滚代码添加到malloc_or_die。

有关使用goto的真实示例,请查看解析使用计算goto的调度代码。