是的,两个讨厌的结构合并。它听起来是不是很糟糕,还是被视为控制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。两人都气馁。在这种情况下,阻止他们的理由是什么?
感谢。
答案 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)开发和开发的。这些模式被称为while
和if
模式等。
因此,如果您使用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的调度代码。