./C pre-processor trick

posted by cli on

C pre-processor trick

本文以一个通信协议中的数据包为例,介绍了一个可以处理很多相似数据结构的C预处理器的技巧。
17 January 2000

概述

这里描述的这个C预处理器技巧并不是我自己发明的,而是在我1997年加入HiBase项目时学到的。一开始我有点憎恶这个技巧,经验告诉我,用C预处理器做任何复杂的事都可能在今后导致问题,不说别的,它会让代码晦涩难懂。但是,我慢慢发现这是一个避免内存分配、初始化以及其他问题的有效方法,我开始欣赏这个技巧。这个技巧不是万能良药,事实上,它的用武之地仅仅局限在几类特定任务中,但它在这些任务中却显得特别有用。 这个技巧是由HiBase项目的Kenneth Oksanen开发的,他告诉我,这个技巧是他在GCC中类似构想的基础上加以发展和完善而来的。我不知道其他人是否已经使用过这个技巧了。

问题描述

一些程序中有许多类似、相关的数据类型,通信协议中表示数据包的大量数据类型就是一个非常好的例子,他们可能看上去像这样:

struct AuthPacket {
    char *username;
    char *password;
};
struct AckPacket {
    int status;
    char *error_text;
};

这些数据结构用来表示在信道中传输的未封装的二进制数据包。未封装的数据包使用方便,但需要函数来封装、拆分、分配(至少需要初始化)、释放这些数据结构,同时也需要函数来产生调试用的转储信息。 写这些函数是个无聊而又容易出错的任务,即使这样,这些函数仍然需要保持同步:如果一个数据包中增加了一个新的域,这个域就需要初始化、释放、封装、拆分以及转储,忘记任何一项都可能导致BUG。

解决方案

这个技巧就是使用预处理器定义一种新的语言来描述数据包的结构:

PACKET(Auth,
    STRING(username)
    STRING(password)
)
PACKET(Ack,
    INTEGER(status)
    STRING(error_text)
)

这些描述放在一个头文件中,比如说packet-desc.h,之后适当的地方包含这个头文件,并恰当地定义PACKET, STRING和 INTEGER这些宏。例如,可以这样来定义真正的数据类型:

struct Packet {
    int type;
    #define INTEGER(name) int name;
    #define STRING(name) char *name;
    #define PACKET(name, fields) \
    struct name { fields } name;
    #include "packet-desc.h"
};

预处理器会将它转换成如下等价的形式:

struct Packet {
    int type;
    struct Auth {
    char *username;
    char *password;
    } Auth;
    struct Ack {
    int status;
    char *error_text;
    } Ack;
};

类似这样地使用头文件可以实现各种函数,另外,还可以定义一个所有数据包类型的枚举类型,以及将这些枚举值和字符串相互转换、用于I/O的的函数。

讨论

这个技巧所要解决的问题同样可以使用高级语言来解决,问题是,我们并不一定能自由选择高级语言。 即使是对C而言,这个技巧也是不靠谱的:它使用了预处理器定义新的语法。C程序员们在痛苦的经历中明白了发明新的语法会让维护代码的程序员感到困惑,由此还会产生BUG。但是,我认为在这个案例中利大于弊,因为这个技巧省去了大量重复的代码。 复杂的预处理器技巧会导致预处理器出现错误,在移植时带来不便。只要预处理器没有问题,这是个相当简单良性的技巧。但它也有它的不足:使用时对宏参数中的逗号要特别小心(它们要包含在括号中);要小心地undefine在头文件中使用的宏,这样才不会在下次包含头文件时使用错误的宏定义。

原文链接