6

是的,两个讨厌的构造结合在一起。它是否像听起来那么糟糕,或者它可以被视为控制 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;

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

谢谢。

4

9 回答 9

11

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

但是,如果我必须维护该代码,我会非常沮丧,因为这些代码goto被宏隐藏了。

所以在这种情况下goto对我来说是可以的,但不是宏。

于 2009-03-31T09:11:18.517 回答
8

使用goto去一个常见的错误处理程序/清理/退出序列是绝对好的。

于 2009-03-31T08:59:25.770 回答
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);
}

内存分配是否成功。

这是有效的,因为 free() 如果传递一个 NULL 指针什么都不做。在设计自己的 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 可以大大简化资源管理。

于 2009-03-31T09:01:50.303 回答
3

Cleanup withgoto是一种常见的 C 习惯用法,用于 Linux 内核*.

**也许 Linus 的观点不是一个好的论证的最佳例子,但它确实表明goto在一个相对较大的项目中被使用。*

于 2009-03-31T09:08:52.283 回答
3

如果第一个 malloc 失败,则清理 p1 和 p2。由于 goto,p2 没有被初始化并且可能指向任何东西。我用 gcc 快速运行以检查并尝试释放(p2)确实会导致段错误。

在您的最后一个示例中,变量的范围在大括号内(即它们仅存在于 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);
}
于 2009-03-31T13:21:41.457 回答
2

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

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

于 2009-03-31T09:06:20.637 回答
2

原始代码将受益于使用多个返回语句 - 无需跳过错误返回清理代码。另外,您通常也需要在普通返回时释放分配的空间 - 否则您会泄漏内存。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 语句并不明显!很明显,对它所来自的代码的罪恶进行完整的解释需要一整天的时间——它们是多种多样的。

于 2009-03-31T13:31:07.440 回答
1

第一个示例对我来说比宏化版本更具可读性。而且mouviciel说的比我好得多

于 2009-03-31T09:02:48.880 回答
0
#define malloc_or_die(size) if(malloc(size) == NULL) exit(1)

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

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

于 2009-04-23T03:31:34.177 回答