【问题标题】:How evil is this foreach C macro?这个 foreach C 宏有多邪恶?
【发布时间】:2011-06-29 22:21:24
【问题描述】:

这个问题的前言是,我意识到 C 宏是一个敏感的主题。很多时候,它们可以通过更安全且不受诸如增量参数等经典问题的非宏观解决方案来完成;所以有了这个,我在 C 中有一个哈希表实现,其中链接节点用于冲突。我敢肯定大多数人已经看过一百万次了,但它有点像这样。

typedef struct tnode_t {
    char* key; void* value; struct tnode_t* next;
} tnode_t;

typedef struct table_t {
    tnode_t** nodes;
    unsigned long node_count;
    unsigned long iterator; // see macro below
        ...
}

我想提供一种遍历节点的抽象方式。我考虑使用一个函数,它接受一个函数指针并将函数应用于每个节点,但我经常发现这种解决方案非常有限,所以我想出了这个宏:

#define tbleach(table, node) \
    for(node=table->nodes[table->iterator=0];\
        table->iterator<table->node_count;\
        node=node?node->next:table->nodes[++table->iterator])\
            if (node)

可以这样使用:

tnode_t* n;
tbleach(mytable, n) {
    do_stuff_to(n->key, n->value);
}

我能看到的唯一缺点是迭代器索引是表的一部分,因此显然您不能在同一个表中同时进行两个循环。我不知道如何解决这个问题,但考虑到这个小宏会有多大用处,我不认为它会破坏交易。所以我的问题。

** 更新 **

我采纳了 Zack 和 Jens 的建议,用“else”消除了问题,并在 for 语句中声明了迭代器。一切似乎都正常,但视觉工作室抱怨在使用宏的地方“不允许使用类型名称”。我想知道这里到底发生了什么,因为它可以编译并运行,但我不确定迭代器的作用域。

#define tbleach(table, node) \
    for(node=table->nodes[0], unsigned long i=0;\
        i<table->node_count;\
        node=node?node->next:table->nodes[++i])\
        if (!node) {} else

这种方法是不好的形式吗?如果不是,有什么方法可以改进它?

【问题讨论】:

  • 我不禁想像邪恶博士。 “它是一个邪恶的宏吗?”
  • 你不能也将迭代器传递给宏吗?例如:tbleach(mytable, iterator, n)... 这将允许您运行多个循环。
  • 您是否反对将第三个参数用作迭代器?
  • 您的哈希表的实际表是否经过优化打包?我认为它们通常是基于散列访问的,所以它们根本不是从 0..n 开始按顺序填充的。此外,仅当您停止关注下一个链接时,迭代器才会增加,但仍与看起来可疑的 node_count 进行比较。
  • @unwind 测试表明,打包大约是 3 个节点中的 1 个,因此虽然没有优化前沿,但我们的速度很快。至于命名node_count,table stuct 中还有一个value_count 的值。命名可能会更好,但这是另一个话题。

标签: c macros hashtable c-preprocessor


【解决方案1】:

唯一真正不可接受的事情就是你已经说过的——迭代器是表的一部分。你应该像这样把它拉出来:

typedef unsigned long table_iterator_t;
#define tbleach(table, iter, node) \
    for ((iter) = 0, (node) = (table)->nodes[(iter)]; \
         (iter) < (table)->node_count; \
         (node) = ((node) && (node)->next) \
                  ? (node)->next : (table)->nodes[++(iter)])

// use:
table_iterator_t i;
tnode_t *n;
tbleach(mytable, i, n) {
    do_stuff_to(n->key, n->value);
}

我还将if 语句吸到了for 循环表达式中,因为这样会稍微安全一些(如果循环体的右括号之后的下一个标记是else,则不会发生奇怪的事情)。请注意,数组条目 table-&gt;nodes[table-&gt;node_count] 将被读取,这与通常的约定不同,因此您需要为其分配空间(并确保它始终为 NULL)。我认为你的版本也是如此。

编辑:更正了表条目为 NULL 的情况的逻辑。

【讨论】:

  • 如果线程在外围的任何地方,像这样的高状态变量肯定会惹上麻烦。
  • 我刚刚意识到我有一个错误 - 如果table-&gt;nodes 中的条目为空,它会崩溃。必须检查循环推进表达式中的nodenode-&gt;next。这接近了一些内联帮助函数可能是一个好主意的地步。
  • 如果表条目中没有节点,您是否还会在循环内遇到 n 为空的情况?
  • 嗯,是的,如果一行中有两个 NULL 表条目。这将是回到嵌套的 if 语句的原因,或者(我的偏好)将循环提前表达式拉到内联帮助器中,以便它可以更复杂。
  • 是的,在尝试了这个之后我发现 - 我的 KISSy 解决方案就是这样做:if (!node) {} else
【解决方案2】:

除了 Zack 关于迭代器和终止 if 的回答可能会混淆语法:

如果您有 C99,请在 for 循环中使用局部变量。这将避免周围范围的变量将持有悬空指针的坏意外。在宏中使用类似的东西:

for(nodeS node = ...

而且,以 _t 结尾的名称由 POSIX 保留。所以最好不要将它们用于您自己的类型。

【讨论】:

    猜你喜欢
    • 2010-10-11
    • 1970-01-01
    • 1970-01-01
    • 2016-11-18
    • 1970-01-01
    • 1970-01-01
    • 2017-01-15
    • 2010-10-02
    • 2011-01-02
    相关资源
    最近更新 更多