C语言中do {...} while (false)

  4 mins to read  

List [CTL]

    最近意外读到一个C语言开源项目,发现一种写法被用了很多次:

    do {
        xxx;
    } while (0)
    

    就是只循环一次的代码段,我去Github上搜了一下,发现很多C/Cpp的项目中都在这样写(223M+),举个Linux 内核的例子,摘自内核源码lib/atomic64_test.c文件

    #define TEST_RETURN(bit, op, c_op, val)         \
        do{                                         \
            atomic##bit##_set(&v, v0);              \
            BUG_ON(atomic##bit##_read(&v) != r);    \
        } while (0)
    

    可以看到这个inline宏用do {...} while (0)执行了两个函数。顺带一提##的用法,它是拼接两个字段(或称token),感觉类似php那种把字符串转义为变量名:

    #include <stdio.h>
    
    int foo_1(){
        printf("1");
    }
    
    int foo_2(){
        printf("2");
    }
    
    int foo_3(){
        printf("3");
    }
    
    #define EXEC(f, c) f##c()
    
    int main(void){
        EXEC(foo_, 1);
        EXEC(foo_, 2);
        EXEC(foo_, 3);
        return 0;
    }
    
    
    // output: 123
    

    回到原题,在参考了Blog1Blog2以及Stackoverflow讨论后,总结这种写法的好处:

    • 用在多行宏里,这是唯一一种使得多行宏在任何代码段里都能正常使用的方法:

      do{…}while(0), is the only construct in C that lets you define macros that always work, the same way, so that a semicolon after your macro always has the same effect, regardless of how the macro is used (with particularly emphasis on the issue of nesting the macro in an if without curly-brackets).

      举个栗子:

      #define MACRO(x) foo(x); bar(x)
      

      当我在代码里使用这个宏:

      int a = 1;
      MACRO(a);
        
      // =>
        
      int a = 1;
      foo(a);
      bar(a);
      

      在以上代码段里它正常运行,而当我把它放进一个不带花括号的if语句里

      if (a!=0)
          MACRO(a);
        
      // =>
        
      if (a!=0)
          foo(a);
      bar(a);
      

      bar函数已经不在if范围内了

      假如说在宏里用花括号包裹多行代码,貌似可以解决这种问题:

      #define MACRO(x) {foo(x); bar(x);}
        
      if (a!=0)
          MACRO(x)		// 这里不能加分号
        
      // =>
        
      if (a!=0){
          foo(x);
          bar(x);
      }
        
      /*****************************************/
        
      if (a!=0)
          MACRO(x)
      else 
          // do sth;
        
      // =>
        
      if (a!=0){
          foo(x);
          bar(x);
      }
      else
          // do sth;
      

      以上的代码段里可以看出为什么不用宏外包裹花括号的方式:

      • 不统一,有的地方加分号,有的地方不加,如果是写函数库的话,显然很蛋疼
      • if后有else时,syntax error

      所以,当用do {...} while (0)时:

      #define MACRO(x) do {foo(x); bar(x);} while (0)
        
      int a = 1;
      if (a!=0)
          MACRO(a);
      else
          //do sth;
            
      // =>
        
      if (a!=0)
          do {
      		foo(a);
              bar(a);
          } while (0);
      else
          // do sth;
      

      它可以保证在任何代码段里正常工作

    • 避免深层嵌套和避免goto语句

      假如说,我在一个函数里malloc了几个变量,在函数调用结束我需要free掉它们:

      int foo(){
          char* a = (char*)malloc(1);
          char* b = (char*)malloc(1);
          int c;
          // do sth;
          free(a);
          free(b);
          return c;
      }
      

      这时假如里面有一系列判断,不符合条件即可直接return,假如没有函数末尾的善后工作,可以这样:

      if (cdn1)
          return c;
      if (cdn2)
          return c;
      

      但是有善后工作需要做,所以所有判断最后都得回到free,如果不用do {...} while (0),有两种写法:

      if (!cdn1){
          if (!cdn2){
              ;
          }
      }
        
      /************************/
        
      if (cdn1)
          goto LABEL;
      if (cdn2)
          goto LABEL;
        
      LABEL: 
      free(a);
      free(b);
      return c;
      

      第一种就是深层嵌套,写超过五层嵌套或迭代代码的人都是应该被拉出去打死的

      第二种就是goto语句,很多书都说过,能不用就不用,因为读着实在累,很多大厂代码规范也明确不允许使用goto

      所以,看看do {...} while (0)怎么实现安全的资源释放

      int foo(){
          char* a = (char*)malloc(1);
          char* b = (char*)malloc(1);
          int c;
          do {
              if (cdn1)
                  break;
              if (cdn2)
                  break;
          } while (0);
          free(a);
          free(b);
          return c;
      }
      

      完美解决了上面的两个问题