8月 292014
 

有时候我们希望在构建时能够在命令行添加一些宏定义,改变程序行为。一个典型应用示例是代码里通过检查是否定义了 DEBUG 宏,来决定是否输出调试信息。编译器一般提供命令行选项支持这种做法,例如使用 gcc 时可以在命令行添加 -D 选项,定义一些宏:

gcc -DDEBUG=1 a.c

不过,大型项目一般都需要自动化构建工具,如 GNU Autotools 或者 CMake。使用它们后,我们无法直接修改编译命令,但它们提供了替代方案。对于 Autotools,它可以识别 CPPFLAGS 等环境变量,从而我们可以在命令行上添加宏定义,例如:

CPPFLAGS="-DDEBUG=1" ./configure

此外,我们还可以先 export CPPFLAGS="-DDEBUG=1",再用 ./configure 命令。注意这里 CPPFLAGS 的 CPP 指的是 C 预处理器 (C Preprocessor),不是 C++,其中只应该设置 -I-D-U 等影响预处理器的选项。Autotools 支持 CFLAGSCXXFLAGSLDFLAGS 环境变量,分别用来指定 C 编译链接时、C++ 编译链接时、以及仅在链接时有效的选项。

目前许多项目已经使用 CMake 来构建。CMake 支持 CFLAGSCXXFLAGSLDFLAGS 等,但遗憾的是唯独不支持 CPPFLAGS。也就是说,CMake 会无视 CPPFLAGS 环境变量的值。Aron Xu 为此报了一个 BUG,不过因为没人志愿去做,所以一直没有修复。许多人可能会以为,可以用类似 cmake .. -DVAR=VALUE 的方式来添加宏定义,但这是无效的。这样一来,如果想在命令行上指定宏定义,只能复用 CFLAGS 或者 CXXFLAGS 变量。这基本可以工作,因为一般情况下预处理和编译是同一个命令进行的。

CMake 中标准的添加宏定义方法是使用 ADD_DEFINITIONS(-DMACRO)(参考文档),不过这需要修改 CMakeLists.txt 文件。如果想要在命令行上定制宏的开关或取值,那么可以像这里一样,定义一个 CMake 选项:

OPTION(DEFINE_MACRO "Option description" ON) # Enabled by default

然后做一个条件判断:

IF(DEFINE_MACRO)
    ADD_DEFINITIONS(-DMACRO)
ENDIF(DEFINE_MACRO)

之后,就可以用 cmake -DDEFINE_MACRO=OFF .. 调整宏的开关或取值了。

最后值得一提的是,如果代码里已经有了宏定义语句,那么通过命令行修改其取值是不可能的。编译器会警告宏被重复定义,而且生效的是源文件中的定义。想要重新定义,只能随后 #undef#define。这将不得不通过修改代码来实现。

8月 282014
 

本文介绍 C++ 中传统枚举类型存在的作用域不受限等问题,随后列举经典的限定其作用域的做法,最后给出新标准 C++11 下的解决方案。

传统行为

传统的枚举类型在 C 语言中就有,C++ 中行为和 C 中一致,常被用来定义有类型的常量。一个典型的枚举类型定义如下:

enum Color { RED, BLUE };

C++ 发明人 Bjarne Stroustrup 总结这种枚举有如下问题

  • 作用域不受限 (unscoped),枚举变量的作用域不受限,会暴露给领近的代码作用域(如果在最外层则为全局作用域),容易引起命名冲突。例如如下代码是无法编译通过的:

    enum Color { RED, BLUE };
    enum Feeling { EXCITED, BLUE };
  • 会隐式转换为 int。这是 C 中的默认行为,但是和“有类型的常量”的初衷是不符合的。比如上面例子中 EXCITED == RED 会返回真(gcc 编译会有警告),这其实是不合常理的。

  • 用来表征枚举变量的实际类型不能明确指定,从而无法支持枚举类型的前向声明。

经典做法

解决作用域不受限带来的命名冲突问题的一个简单方法是,给枚举变量命名时加前缀,如上面例子改成 COLOR_BLUE 以及 FEELING_BLUE。一般说来,为了一致性我们会把所有常量统一加上前缀。但是这样定义枚举变量的代码就显得累赘。C 程序中可能不得不这样做。不过 C++ 程序员恐怕都不喜欢这种方法。替代方案是命名空间:

namespace Color { enum Type { RED, YELLOW, BLUE }; };

这样之后就可以用 Color::Type c = Color::RED; 来定义新的枚举变量了。如果 using namespace Color 后,前缀还可以省去,使得代码简化。不过,因为命名空间是可以随后被扩充内容的,所以它提供的作用域封闭性不高。在大项目中,还是有可能不同人给不同的东西起同样的枚举类型名。

更“有效”的办法是用一个类或结构体来限定其作用域,例如:

struct Color { enum Type { RED, YELLOW, BLUE }; };

定义新变量的方法和上面命名空间的相同。不过这样就不用担心类在别处被修改内容。这里用结构体而非类,一是因为本身希望这些常量可以公开访问,二是因为它只包含数据没有成员函数。

C++11 的枚举类

上面的做法解决了第一个问题,但对于后两个仍无能为力。庆幸的是,C++11 标准中引入了“枚举类”(enum class),可以较好地解决上述问题。它使用如下语法定义:

enum class Color { RED, BLACK };

可见语法更为简单了。如此一来,定义新变量也得到简化:

Color c = Color::RED;

类限制了其作用域,避免了命名冲突。同时也避免了隐式类型转换。也就是说,枚举类即是作用域受限的 (scoped),又是强类型的 (strongly typed) 枚举。至于第三个问题,C++11 标准允许指定存储类型:

enum class Color : char { RED, BLUE };

上面例子使用 char 来存储这个枚举类。缺省情况下使用 int。这样枚举就可以进行前向声明了:

enum class Color : char ;   // forward declaration
void foo (Color *p);
// ...
enum class Color : char { RED, BLUE }; // definition

有了前向声明,代码可以更好地组织到不同的文件里,增加程序可读性和可维护性。

更多参考

除了文中链接外,还有如下链接可供参考: