do{...}while(0)在宏定义中的应用

今天在看linux内核中链表操作的接口的时候,碰到这样一个宏定义:

1
2
3
4
// INIT_LIST_HEAD宏用于运行时初始化链表
#define INIT_LIST_HEAD(ptr) do { \
(ptr)->next = (ptr); (ptr)->prev = (ptr); \
} while (0)

不太明白这里的do{...}while(0)是个什么操作,看起来似乎没有什么用——因为这里while(0)显然并没有起到循环的效果。后来查了下资料(网址在这),才知道在Linux内核和其它一些著名的C库中有许多使用do{…}while(0)的宏定义,也大概明白了这么写的作用。

  • 第一个理由:空的 statement 会让编译器发出警告,所以会看到有些宏定义是这样的: #define FOO do{ }while(0) 。(暂时没看到这样的宏 = =。 先记录一下

  • 第二个理由:它提供了一个 block 用于声明局部变量。可能你会想到不使用 do{...}while(0) 而简单地使用一对大括号 {...} 。这样有缺陷,具体看下一条。

  • 第三个理由:让你能够声明复杂的宏定义。想象一下一个宏定义如下:

    1
    2
    3
    #define FOO(x) \
    printf("arg is %d\n", x); \
    do_something_useful(x);

    现在这么使用它:

    1
    2
    if (blah == 2)
    FOO(blah);

    宏本质上就是文本替换,所以它实际上:

    1
    2
    3
    if (blah == 2)
    printf("arg is %d\n", blah);
    do_something_useful(blah); // 不论 blah为何止,都会执行这条

    所以显然,这可能带来用于预期之外的效果。而如果用了 do{...}while(0) ,就会是这样的:

    1
    2
    3
    4
    5
    6
    if (blah == 2)
    do {
    printf("arg is %d\n", blah);
    do_something_useful(blah);
    } while (0);
    // OK

    可能有人会想,既然需要一个 block ,那么加个大括号不就好了?好的,假如说有这样一个宏定义:

    1
    2
    3
    4
    5
    6
    7
    // 交换两个值
    #define exch(x, y) { int tmp; tmp = x; x = y; y = tmp; }

    if (x > y)
    exch(x, y); // 看起来似乎没问题
    else
    do_something();

    很显然,这个相当于这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    if (x > y)
    {
    int tmp;
    tmp = x;
    x = y;
    y = tmp;
    }
    ; // 注意这里!!
    else // 语法错误
    do_something();

    问题就出现在那个 ; 。当然,你可以选择当初写下 each(x, y) 的时候在这行末尾不加分号,但是(我觉得)这实在显得太奇怪了。

    用了 do{...} while (0) 就没有这个问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    if (x > y)
    do {
    int tmp;
    tmp = x;
    x = y;
    y = tmp;
    } while (0); // Ok
    else
    do_something();

总结:Linux和其它代码库里的宏都用 do{...}while (0) 来包围执行逻辑,因为它能确保宏的行为总是相同的,而不管在调用代码中使用了多少分号和大括号。

(一点吐槽:感觉就是给C语言的宏擦屁股的?还有就是 if/else 即使只有一条语句也加大括号真是个好习惯 = =。