【问题标题】:GCC macro expansion to call another macroGCC 宏扩展来调用另一个宏
【发布时间】:2014-01-16 14:29:58
【问题描述】:

我正在用 C (gcc) 编写一个应用程序,它会进行大量的字符串比较。总是一个带有一长串编译时常量字符串的未知/动态字符串。 所以我想我散列动态字符串并将生成的散列与常量字符串的预计算散列进行比较。

这样做我在一个函数(用于动态运行时字符串)和一个宏中有哈希算法,以便 gcc 在编译时评估哈希。

我知道了:

#define HASH_CALC(h, s) ((h) * 33 + *(s))
#define HASH_CALC1(s) (HASH_CALC(hash_calc_start, s))
#define HASH_CALC2(s) (HASH_CALC(HASH_CALC1(s), s + 1))
#define HASH_CALC3(s) (HASH_CALC(HASH_CALC2(s), s + 2))
#define HASH_CALC4(s) (HASH_CALC(HASH_CALC3(s), s + 3))
#define HASH_CALC5(s) (HASH_CALC(HASH_CALC4(s), s + 4))
//--> cut ... goes till HASH_CALC32

static const unsigned long hash_calc_start = 5381;
unsigned long hash_str (const char* c);

void func () {
    //This string is not constant ... just in this case to show something
    char dynStr = "foo";
    unsigned long dynHash = hash_str (dynStr);

    //gcc produces a cmp with a constant number as foo is hashed during compile-time
    if (dynHash == HASH_CALC3("foo")) {
    }
}

现在问题来了:

是否可以创建一个扩展为 HASH_CALCX(s) 的宏,其中 X 是传递给宏的常量字符串的长度?

//Expands to HASH_CALC3("foo")
if (dynHash == HASH_CALCX("foo")) {
}
//Expands to HASH_CALC6("foobar")
if (dynHash == HASH_CALCX("foobar")) {
}

我试过了,但它不起作用。

#define HASH_STRLEN(x) (sizeof(x)/sizeof(x[0])-1)
#define HASH_MERGE(x,y) x ## y
#define HASH_MERGE2(x,y) HASH_MERGE(x,y)
#define HASH_CALCX(s) (HASH_MERGE2(HASH_CALC, HASH_STRLEN(s))(s))

谢谢!

【问题讨论】:

  • 没有必要。 strlen 非常适合字符串常量,返回一个编译时常量。
  • @Damon Well ... #define HASH_CALCX(s) (HASH_MERGE2(HASH_CALC, strlen(s))(s)) 也不起作用,因为 strlen("") 没有展开。
  • hash_calc_start 是如何定义的? #define hash_calc_start 0?
  • 其实是 st​​atic const unsigned long hash_calc_start = 5381;但是如果这对我的问题有帮助的话,将它定义为宏常量是没有问题的:-)
  • 您是否曾尝试只调用散列函数而不是使用宏?如果函数定义在同一个翻译单元中,任何现代编译器都应该能够内联它并最终减少为一个常量。

标签: c gcc macros


【解决方案1】:

你可以使用三元运算符:

#define HASH_CALCX(s)                \
   (strlen(s) == 5 ? HASH_CALC5(s) : \
    strlen(s) == 4 ? HASH_CALC4(s) : \
    strlen(s) == 3 ? HASH_CALC3(s) : \
    strlen(s) == 2 ? HASH_CALC2(s) : \
    strlen(s) == 1 ? HASH_CALC1(s) : some_error)

这是否可行取决于编译器的优化,将这个表达式减少为单个数字常量。

更新:使用 gcc 完成的示例。如果优化级别设置为 1 则很好,但不设置为 0。

设 fox.c 为:

#include <string.h>
#include <stdio.h>

#define HASH_CALC(h, s) ((h) * 33 + *(s))
#define HASH_CALC1(s) (HASH_CALC(5381, s)) // hash_calc_start = 5381
#define HASH_CALC2(s) (HASH_CALC(HASH_CALC1(s), s + 1))
#define HASH_CALC3(s) (HASH_CALC(HASH_CALC2(s), s + 2))
#define HASH_CALC4(s) (HASH_CALC(HASH_CALC3(s), s + 3))
#define HASH_CALC5(s) (HASH_CALC(HASH_CALC4(s), s + 4))

#define HASH_CALCX(s)                \
   (strlen(s) == 5 ? HASH_CALC5(s) : \
    strlen(s) == 4 ? HASH_CALC4(s) : \
    strlen(s) == 3 ? HASH_CALC3(s) : \
    strlen(s) == 2 ? HASH_CALC2(s) : \
    strlen(s) == 1 ? HASH_CALC1(s) : 0)

int main(void) {
   printf("%d\n", HASH_CALCX("foo"));
   return 0;
}

然后gcc -S -O1 foox.c 给出:

        .file       "foox.c"
        .section    .rodata.str1.1,"aMS",@progbits,1
.LC0:
        .string     "%d\n"
        .text
.globl main
        .type       main, @function
main:
        leal        4(%esp), %ecx
        andl        $-16, %esp
        pushl       -4(%ecx)
        pushl       %ebp
        movl        %esp, %ebp
        pushl       %ecx
        subl        $20, %esp
        movl        $193491849, 4(%esp)
        movl        $.LC0, (%esp)
        call        printf
        movl        $0, %eax
        addl        $20, %esp
        popl        %ecx
        popl        %ebp
        leal        -4(%ecx), %esp
        ret
        .size       main, .-main
        .ident      "GCC: (GNU) 4.1.2 20080704 (Red Hat 4.1.2-54)"
        .section    .note.GNU-stack,"",@progbits

更新 2:作为一个次要增强,我肯定会尝试添加一个编译时“断言”来验证只有 literal一定长度的字符串被传递给宏,因为我很容易出错。例如,将上面的内容修改为:

#define ASSERT_zero(e) (!sizeof(struct{int:!!(e);}))
#define HASH_CALCX(s)  (ASSERT_zero(strlen(s) <= 5) + \
   (strlen(s) == 5 ? HASH_CALC5(s) : \
   etc

ASSERT_zero() 宏类似于 BUILD_BUG_ON_ZERO(),并使用“位域大小”技巧。它产生:

  • 编译错误,当e 为假时,或
  • 零值。

ASSERT_zero() 不适用于 C++。而且这个额外的检查不适用于 VS IIRC,因为 VS 不会将 strlen("foo") 视为编译时常量。

【讨论】:

  • 我可以确认这是可行的 --> 生成一个编译时常量。因此,如果没有给出更好的答案(解决方案),我可以使用它是一个有效的解决方法......到目前为止谢谢! (实际上我什至不喜欢 HASH_CALC1/HASH_CALC2/HASH_CALC3 的东西。这样我会得到一个更多的怪物宏)
【解决方案2】:

不幸的是,在 C 中,字符串分解不是一个常量表达式。以下内容无效:

switch (x) {
case "a"[0]:
  ...
}

预处理器中的常量表达式是相似的。以下内容无效:

#if "a"[0] == 'a'
  ...
#endif

如果您不需要在switch 语句中使用哈希码,为什么不在程序的初始化阶段预先计算这些值,将结果存储到变量中,然后简单地在测试中使用它们?

如果您在switch 语句中需要它们,我建议将字符串拆分为字符。在您的情况下,这将给出如下宏:

#define HASH_CALC(...) HASH_CALC0(5381, __VA_ARGS__, 0, 0, 0, 0, 0)
#define HASH_CALC0(p, x, ...) (x == 0 ? (p) : HASH_CALC1((p)*33+x, __VA_ARGS__)))
#define HASH_CALC1(p, x, ...) (x == 0 ? (p) : HASH_CALC2((p)*33+x, __VA_ARGS__)))
#define HASH_CALC2(p, x, ...) (x == 0 ? (p) : HASH_CALC3((p)*33+x, __VA_ARGS__)))
#define HASH_CALC3(p, x, ...) (x == 0 ? (p) : HASH_CALC4((p)*33+x, __VA_ARGS__)))

...
#define HASH_CALC64(p, x, ...) (x == 0 ? (p) : -1 /* shall never be used */)

HASH_CALC 需要可变数量的字符作为参数。例如,以下是有效代码:

switch (x) {
case HASH_CALC('f', 'o', 'o'):
   ...
case HASH_CALC('f', 'o', 'o', 'b', 'a', 'r'):
   ...
}

【讨论】:

  • 我喜欢你的回答,就像你说你的宏也可以在 switch 语句中使用一样。只有一个难题:为什么宏中的 (x+0) 而不是 x?
  • @xatian:这是一个技巧。有 64 个宏最多使用 64 个参数。但是,如果参数较少,则剩余的宏将空字符串作为参数。所以x 可能为空并且会给出语法错误,因为表达式将是( == 0 ? ...) 加上+0,这给出( (+0) == 0 ? ...)。它不是 100% 干净的:)。这让我觉得您可能不需要在 HASH_CALC 参数中输入尾随 0。
  • 嗯...我测试了一下,您的宏似乎反映了传递给它的字符串 --> 生成的哈希是错误的。经过一番思考,这是有道理的:-)
【解决方案3】:

尽管使用宏可以做的事情令人惊奇,但这是最好/最可维护的方法吗?

我倾向于将字符串放在另一个文件中,并使用 perl 脚本生成散列并以某种方便的数据结构输出包含字符串和散列的 c++ 源文件。

您的 makefile 可以设置为在字符串更改时重新运行 perl 脚本。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-07-06
    • 1970-01-01
    • 2022-09-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多