【问题标题】:Building a C iterator macro with a pre-C99 compiler使用 C99 之前的编译器构建 C 迭代器宏
【发布时间】:2014-03-07 21:54:43
【问题描述】:

我的代码必须使用 C99 之前的编译器进行编译(我们正在努力更新,但这是一项艰巨的任务),它正在调用以 C99 为设计理念的实用程序库。特别是,这些实用程序定义了一个 hashmap 类型并提供了一个宏来遍历它,类似于以下内容:

#define MAP_FOREACH(key, val, map) \
    for (struct _map_iterator iter __attribute__((cleanup(_map_iter_cleanup))); \
        (key) = iter->pair->key, \
        (value) = iter->pair->value; \
        iter = iter->get_next_cb())

实际代码有更多内容(确保迭代器名称唯一的功能等),但这涵盖了我的问题的实质,即 1999 年以前的 C 版本不支持初始化变量在 for 循环内。现在明显的解决方法是将初始化移到循环之外,代码如下:

// Doesn't work
#define MAP_FOREACH(key, val, map) \
    struct _map_iterator iter __attribute__((cleanup(_map_iter_cleanup)));
    for (; \
        (key) = iter->pair->key, \
        (value) = iter->pair->value; \
        iter = iter->get_next_cb())

问题在于__attribute__((cleanup(_map_iter_cleanup))) 位。根据GCC documentation,它定义了附加到变量的清理代码,当变量超出范围时运行。因为我们将迭代器声明移到了 for 循环之外,所以它的范围发生了变化,并且清理代码在其他地方运行。不幸的是,库的其他部分依赖于立即清理迭代器 - 映射会跟踪已启动的迭代器数量,如果在它们全部消失之前被销毁,则会引发错误。

几天来,我一直在尝试并没有想出一个干净的方法,但我做不到。我真的不想在 C99 之前的代码中重新实现宏加清理,但是这个库到处都在使用,并且更改 API 以包含迭代后清理调用会很痛苦,而不是提到不雅。

有没有人遇到过这种事情并知道解决方法?

编辑:我们使用带有 -std=c89 选项的 GCC 4.2.2

【问题讨论】:

  • 我有点惊讶有一个编译器不支持-std=c99,但支持__attribute__((cleanup()))。这是什么编译器?
  • 哦,原来does支持C99。如果由于某种原因您不能在整个项目中使用 C99,为什么不只对有问题的文件使用它呢?

标签: c c-preprocessor c89


【解决方案1】:

我并不完全了解 C99 之前标准缺少的功能,但我认为您可以这样做:

#define MAP_FOREACH(key, val, map) \
    {
        struct _map_iterator iter __attribute__((cleanup(_map_iter_cleanup)));
        for (; \
            (key) = iter->pair->key, \
            (value) = iter->pair->value; \
            iter = iter->get_next_cb())

然后无论你在哪里使用它,你都必须在循环后面附加一个额外的右花括号}

【讨论】:

  • 这可能行得通。然而,正如我在上面所说的,这个库到处都在使用(我们的大部分代码库都是 C99-OK)。这会导致大量代码流失并到处进行更改,但更重要的是,这不是一种直观的模式 - 您需要仔细阅读库以了解为什么您的代码需要额外的花括号来编译。跨度>
  • 如果您这样做,请定义另一个宏 #define MAP_FOREACH_END } 来封装右大括号,并确保您对每个 MAP_FOREACH 只使用一次。
【解决方案2】:

一种可能性是创建一个必须在 C89 代码中使用的替代宏。您必须扩展范围并封装操作,例如通过如下方式创建一组新宏:

/* Force a compiler error if non-C99 code uses the C99 macro. */
#if __STDC_VERSION__ >= 199901L
/* C99 code */
#define MAP_FOREACH(key, val, map) \
    for (struct _map_iterator iter __attribute__((cleanup(_map_iter_cleanup))); \
        (key) = iter->pair->key, \
        (value) = iter->pair->value; \
        iter = iter->get_next_cb())
#endif

/* C89-compatible macro */
#define MAP_FOREACH_DO(key, val, map, statement) \
do{ struct _map_iterator iter __attribute__((cleanup(_map_iter_cleanup))); \
for (; \
    (key) = iter->pair->key, \
    (value) = iter->pair->value; \
    iter = iter->get_next_cb()) \
    { \
        statement; \
    } \
}while(0)

对于简单的语句,您可以执行以下操作:

MAP_FOREACH_DO(..., printf(%s=%s\n", key, value));

...对于更复杂的语句,您可以调用函数。

当然,理想情况下,您只需切换到 C99(至少对于相关代码)并完成它。

【讨论】:

  • 我认为这就是我将要采用的方式。这不是一个漂亮的解决方案,但它应该可以解决问题。
  • 我最终编写了一个 MAP_FN_FOREACH,它接受地图、一个指向用户定义上下文的指针以及一个使用签名 fn(void *context, const char *key, void *value 调用的函数)。
【解决方案3】:

你也许可以逃脱类似的事情:

#define MAP_FOREACH(key, val, map) \
struct _map_iterator iter;
for (; \
    (key) = iter->pair->key, \
    (value) = iter->pair->value ? 1 : _map_iter_cleanup(&iter), 0; \
    iter = iter->get_next_cb())

在循环条件为假时显式调用清理函数。

不幸的是,如果有任何通过break; 退出的MAP_FOREACH 循环,这将失败——迭代器不会被清理。您可以通过添加另一个宏来解决这些问题:

#define MAP_FOREACH_BREAK  { _map_iter_cleanup(&iter); break; }

并用这个宏替换所有有问题的中断。

【讨论】:

  • return 也应该被包装。这种方法的问题在于,如果有人忘记使用MAP_FOREACH_BREAK 而不是简单地使用break,代码将编译,甚至可能看起来可以工作,只是迭代器不会被清理。
【解决方案4】:

gcc 有语句表达式,包含局部变量,几乎是forever,包括typeof 扩展:

({ int y = foo (); int z;
   if (y > 0) z = y;
   else z = - y;
   z; })

(以上链接指向 gcc-2.95.2 手册!)。所以你应该已经拥有了所有的东西来构建更清洁的东西。

【讨论】:

  • 语句表达式用于将语​​句或块视为计算结果为值的表达式。 OP 必须编写一个扩展为 for 循环的宏,该循环具有非常具体的(和编译器相关的)循环迭代器变量语义。您建议如何使用语句表达式来实现这一目标?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-11-19
  • 2013-12-25
  • 1970-01-01
  • 1970-01-01
  • 2012-11-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多