do-while

一、帮助定义复杂的宏以避免错误

假设你需要定义这样一个宏:

#define DOSOMETHING()  foo1(); foo2();

这个宏的本意是:当调用 DOSOMETHING() 时,函数 foo1() 和 foo2() 都会被调用。但是如果你在调用的时候这么写:

if(a > 0)
    DOSOMETHING();

因为宏在预处理的时候会直接被展开,你实际上写的代码是这个样子的:

if(a>0)
    foo1();
    foo2();

这就出现了问题。因为无论 a 是否大于 0,foo2() 都会被执行,导致程序出错。

那么仅仅使用 {} 将 foo1() 和 foo2() 包起来行么?比如:

#define DOSOMETHING() { foo1(); foo2(); }

我们在写代码的时候都习惯在语句右面加上分号,如果在宏中使用 {},代码里就相当于这样写了:{...};,展开后就是这个样子:

if(a > 0)
{
    foo1();
    foo2();
};

很明显,这是一个语法错误(花括号后多了一个分号,虽然不会报错)。

在所有可能情况下,期望我们写的多语句宏总能有正确的表现几乎是不可能的。你不能在没有 do/while(0)的情况下让宏表现的像函数一样。如果我们使用 do{...}while(0) 来定义宏,即:

#define DOSOMETHING() \
do{ \
    foo1();\
    foo2();\
}while(0)\

这样,宏被展开后,上面的调用语句才会保留初始的语义。do 能确保大括号里的逻辑能被执行,而 while(0) 能确保该逻辑只被执行一次,就像没有循环语句一样。

总结:在 Linux 和其它代码库里的,很多宏实现都使用 do/while(0) 来包裹他们的逻辑,这样不管在调用代码中怎么使用分号和大括号,而该宏总能确保其行为是一致的。

如果你开发了一个库,甚至就只是一个宏实现提供给别人使用,你并不能确信所有人都会按照你的意图正确使用该实现,有的使用者处处加大括号,也有的人认为你会处理的。因此你需要保证你的实现在各种使用场景下都是表现一致的。另外,就像 Scott Meyers 在 Effective C++ 中谈及的,让接口容易被正确使用,而不易被误用。

二、避免使用 goto 控制程序流

在一些函数中,我们可能需要在 return 语句之前做一些清理工作,比如释放在函数开始处由 malloc 申请的内存空间,使用 goto 总是一种简单的方法:

int foo()
{
    somestruct * ptr = malloc(...);
    ...;

    if(error)
        goto END;
    ...;  // dosomething
    if(error)
        goto END;
    ...;  // dosomething

    END:
        free(ptr);
        return 0;
}

但由于 goto 不符合软件工程的结构化,而且有可能使得代码难懂,所以很多人都不倡导使用,这个时候我们可以使用 do{...}while(0) 来做同样的事情:

int foo()
{
    somestruct * ptr = malloc(...);
    do
    {
        ...;   // dosomething
        if(error)
            break;
        ...;   // dosomething
        if(error)
            break;
        ...;   // dosomething
    } while(0);

    free(ptr);
    return 0;
}

将函数主体部分使用 do{...}while(0) 包含起来,使用 break 来代替 goto,后续的清理工作在 while 之后,现在既能达到同样的效果,而且代码的可读性、可维护性都要比上面的 goto 代码好的多了。

比如有个人认为这样会导致很多变量要在 do-while 语句外面提前声明(这是 C++ 程序反对的一点,用时才声明),也有人认为还有强大的 RAII 和智能指针神马的,不怕内存问题。但是,在某些场景下,要是这些都不能用呢,比如 C 语言?比如没有 smart ptr?比如维护的是旧代码?

三、避免由宏引起的警告

内核中由于不同架构的限制,很多时候会用到空宏。在编译的时候,这些空宏会给出 warning,为了避免这样的 warning,我们可以使用 do{...}while(0) 来定义空宏:

#define EMPTYMICRO do{}while(0)

四、定义单一的函数块来完成复杂的操作

如果你有一个复杂的函数,变量很多,而且你不想要增加新的函数,可以使用 do{...}while(0),将你的代码写在里面,里面可以定义变量而不用考虑变量名会同函数之前或者之后的重复。

这种情况应该是指一个变量多处使用(但每处的意义还不同),我们可以在每个 do-while 中缩小作用域,比如:

int key;
string value;
int func()
{
    int key = GetKey();
    string value = GetValue();

    ...;  // dosomething for key, value;
    
    do {
        int key;
        string value;
        ...;  // dosomething for this key, value;
    } while(0);    
}
posted @ 2020-03-15 14:06  和风细羽  阅读(325)  评论(0编辑  收藏  举报