不那么坏的宏

做学生时, 写个”Hello World”, 若是Linux, 用个g++ main.cpp就可以了. 但对多些的源码工程, 一行命令就难以搞定了. 这个时候, 大家熟悉的make命令就派上用场. 这个书上很少教说, 我在网上边搜边学后折腾了个简单的makefile, 应付简单的工程…

1
ifeq ($(dd),debug) 
export dd := debug
BIN_DIR=./debug
CPPLAGS=-g -pg -fPIC -Wall -D_FILE_OFFSET_BITS=64
BUILDS=$(DIRS) lib test
else  #release
export dd := release
BIN_DIR=./release
CPPLAGS=-O3 -fPIC -D_FILE_OFFSET_BITS=64
BUILDS=$(DIRS) lib
endif

TEST=
DIRS=$(BIN_DIR)

BASE_DIR=.
LIB_PATH=$(BASE_DIR)/lib
SHARE_PATH=$(LIB_PATH)
STATIC_PATH=$(LIB_PATH)

SHARE_LIBS=boost_thread
STATIC_LIBS=libboost_system.a libevent.a

INC_PATH=./
SRC_DIRS=./ ./bits ./algo ./ext
SRCS=$(foreach SRC_DIRS,$(SRC_DIRS),$(wildcard $(SRC_DIRS)/*.cxx))
OBJS=$(patsubst %.cxx,%.o,$(SRCS))
INC_SHARE=$(addprefix -L, SHARE_PATH/) $(addprefix -l, $(SHARE_LIBS)) 
INC_STATIC=$(addprefix $(STATIC_PATH)/, $(STATIC_LIBS))

.SECONDARY :%.o %(OBJS)

%.o:%.cxx
	@echo "Compile $@ ($<)."
	@g++ $(CPPLAGS) -I $(INC_PATH) -c $< -o $@ 

all: $(BUILDS)

$(DIRS):
	mkdir -p $@

lib: $(OBJS)
	@echo 'build static lib'
	@ar -rc libbang.a $(OBJS)
	mv $(OBJS) libbang.a $(BIN_DIR)

test:
	@echo 'build unit test'
	g++ -DUNIT_TEST -DTEST_$(TEST) $(CPPLAGS) -o $(TEST).exe $(SRCS) -I $(INC_PATH) $(INC_SHARE) $(INC_STATIC) -lcurl

install:
	@echo 'install lib'
	cp $(BIN_DIR)/libbang.a $(LIB_PATH)

clean:
	@echo 'clean up the generated files'
	rm ./debug/ -rf;
	rm ./release/ -rf;

.PHONY: all lib test install clean $(DIRS)

用这个Makefile文件, 即可编译链接了. make dd=debug进行debug方式的编译. 而make dd=release则进行release编译, 以适合生产环境的需要和性能优化. 那这种区别编译是怎么做到的呢? 我们如何在实际使用时干预已经写好的代码行为?答案就是本篇的主题, 宏.

如上的Makefile文件中, 有定义CPPLAGS, 这个就是传输给编译器的宏, 在编译的预处理期可以宏展开, 以替换源码中间相关的宏定义. 像这里就定义了FILE_OFFSET_BITS(在Makefile里会用-Dxxx来定义xxx宏). 这个宏直接作用到了系统的features.h文件, 进而作用到很多头文件, 以满足我们使用64位的大文件的需要. 以stdio.h为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
// features.h的相关定义

#if defined _FILE_OFFSET_BITS && _FILE_OFFSET_BITS == 64
# define __USE_FILE_OFFSET64 1
#endif

// stdio.h的相关定义

# ifndef __USE_FILE_OFFSET64
typedef __off_t off_t;
# else
typedef __off64_t off_t;
# endif

我们在CPPFLAGS里定义了FILE_OFFSET_BITS, 那么编译时会展开typedef __offset_t off_t, 这样的话就能使用大文件, 不会被著名的4G文件困扰.

当初的百度云盘的文件仅能支持4G以内的文件, 和windows32位系统类似, 我的第一反应是不是没定义这个宏啊:)–玩笑了

其实不仅仅是这一点, 我们只要随便翻看开源的C代码项目, 都能看到其Makefile里有各种各样的宏定义以方便条件编译, 跨平台, 定制所使用的库, 控制软件版本, 遵守哪种标准/协议等等.

不仅仅是Linux, 在windows下也是如此. 虽然VS对编译项做了层层包裹, 形成令人眼花缭乱的编译控制面板, 但本质上也是一样的, 也是通过宏来控制程序特性.如下图中的cocos2d的一处windows工程配置, 通过定义一些系统的宏(WIN32, _DEBUG, _WINDOWS, …) 来规制程序的基本形态, 又同时可定义一些工程用的宏来定制程序特性,如COCOS2D_DEBUG=1, 来作为DEBUG开关, 控制一些DEBUG模式下的输出, COCOS2DXWIN32_EXPORTS来进行动态库的导出编制.

win-cocos2d.png

总的来说, 宏在编译时起着开关的作用, 串联系统需要和程序特性, 让开发的代码尽可能地复用, 而又不至于在编译使用时过于繁杂. 一般说来, 宏开关主要控制以下几个方面:

  1. 跨平台兼容. 很多程序/库需要跨平台, 用诸如_WIN32, _GNUC等来控制某些代码的条件编译, 以适配不同的系统API
  2. 依赖库的定制, 像有些库对STL是不大感冒的, 但出于礼貌会考虑兼容一下, 这个时候就给出一个宏, 让你自己来选择是否用STL的一些容器, 不选就默认是自己的容器了.
  3. 基本单位/变量的控制, 上述的64位文件即为如此. 为了兼容多种格式的文件, 宏只得出马. 同样的还有时间time_t等
  4. 函数行为控制, 著名的cplusplus来兼容C函数和C++函数的编译(这两种的编译后的函数是不一样), 还有stdcalll, cdecl, pascal等
  5. 工程定制. 有些项目可能会基于同一套代码编译出库,可执行程序,测试用例程序等三种形式出来. 曾经见过一个项目, 用来同一套代码, 用宏和选择源码编译的方式, 编译出10几种程序来, 个个不同…

不仅仅在条件编译时, 宏可以很好地作为开关来使用. 在代码实现时, 宏的身影也无处不在. 我们可以用宏做一些方便的事儿,而并不会带来太大的麻烦.

  • 如下的一个示例程序, 分别用宏和静态变量定义了一组变量, 两者在代码量上是相当的, 逻辑也足够清晰.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>

#ifdef USE_MACRO

#define PI 3.1415926
#define MAX 4096
#define PRODUCT "badibu.com"
#define VERSION "1.0.2"
#define PRODUCT_INFO PRODUCT"-"VERSION

#else
static const double PI = 3.1415926;
static const size_t MAX = 4096;
static const char* PRODUCT = "badibu.com";
static const char* VERSION = "1.0.2";
static const char* PRODUCT_INFO = "badibu.com-1.0.2";

#endif


int main() {
float a = PI;
double b = PI;
int c = PI;
printf("PI %f %lf %d\n", a, b, c);
int sz = 123;
if( sz < MAX )
printf("%s\n", PRODUCT_INFO);
return 0;
}
  • 以上小程序的执行结果:
1
xxx:~$ g++ -Wall t.cpp 
t.cpp: 在函数‘int main()’中:
t.cpp:27:14: 警告: 在有符号和无符号整数表达式间比较 [-Wsign-compare]
t.cpp: 在全局域:
t.cpp:14:20: 警告: ‘PRODUCT’定义后未使用 [-Wunused-variable]
t.cpp:15:20: 警告: ‘VERSION’定义后未使用 [-Wunused-variable]

xxx:~$ ./a.out 
PI 3.141593 3.141593 3
badibu.com-1.0.2

xxx:~$ g++ -DUSE_MACRO t.cpp 

xxx:~$ ./a.out 
PI 3.141593 3.141593 3
badibu.com-1.0.2

可以看到用宏的话, 有一些小小的优点. 一则避免了不必要的警告, 而同样的效果在不用宏时, 得选择手动地审阅并强制类型转换. 二则对于此例中的PRODUCT_INFO, 用宏的话可以很讨巧地用宏拼接即可, 而不用宏的话, 则得实现个拼接函数才能达到相似效果.

当使用宏定义时, 若并不增加歧义性, 而简洁了代码, 那懒惰的我建议你, 不妨用用.

而使用宏函数或者宏片段时, 有时候更是能做到事半功倍. 前段时间在一个项目中用到了curl库, 这个库对函数进行了很有意思的封装, 有一整套的curl_easy_setopt(xxx)函数, 且其函数系有统一的错误处理机制, 会返回CURLCODE值, 并用curl_easy_strerror获取错误信息. 针对这一特点, 我定制了以下的宏片段, 以节省代码编写, 也显得语义统一.

++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#define CALL(s, x) do {    \
CURLcode ret = x; \
if( 0 != ret ) { \
SELOG(s, "Call "#x" error [%d:%s]\n", ret, curl_easy_strerror(ret)); \
return -1; \
} \
}while(0);



int HttpClient::Init() {
if( NULL == m_node.curl) {
m_node.curl = curl_easy_init() ;
}
CURL* curl = m_node.curl;

string err;
// --------base set opts, 作为基本参数
// 暂不支持gzip,需要设置encoding
CALL(err, curl_easy_setopt(curl, CURLOPT_ENCODING, ""));
// 设置Agent
//string agent = "Mozilla/5.0 (Windows NT 5.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.66 Safari/537.36 LBBROWSER";
string agent = "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.86 Safari/537.36";
CALL(err, curl_easy_setopt(curl, CURLOPT_USERAGENT, agent.c_str()));
//设置重定向的最大次数
CALL(err, curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5));

// ... some code more

return 0;
}

从该片段可以看到, 这里的宏片段很难用普通函数替代. 他的逻辑和调用处的函数逻辑是一体的, 只是因为某些原因重复或者相似代码太多, 故而使用宏片段来替换. 其实这种取巧在很多项目里都会有, 包括咱们的C++标准库:

++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// c++/4.6.3/valarray

#define _DEFINE_VALARRAY_UNARY_OPERATOR(_Op, _Name) \
template<typename _Tp> \
inline typename valarray<_Tp>::template _UnaryOp<_Name>::_Rt \
valarray<_Tp>::operator _Op() const \
{ \
typedef _UnClos<_Name, _ValArray, _Tp> _Closure; \
typedef typename __fun<_Name, _Tp>::result_type _Rt; \
return _Expr<_Closure, _Rt>(_Closure(*this)); \
}


_DEFINE_VALARRAY_UNARY_OPERATOR(+, __unary_plus)
_DEFINE_VALARRAY_UNARY_OPERATOR(-, __negate)
_DEFINE_VALARRAY_UNARY_OPERATOR(~, __bitwise_not)
_DEFINE_VALARRAY_UNARY_OPERATOR (!, __logical_not)

#undef _DEFINE_VALARRAY_UNARY_OPERATOR

该部分宏在template类valarray中实现, 以简化相似函数的实现. 可以看到, 这部分宏函数是template的模板实现, 只是其模板对象是函数名和类型名, 而这个若换成template自身去实现,估计模板的作者都得头大一回. 而且其一旦出错, 其调试信息基本上也是不可调试的…

除了这些取巧的地方, 其实宏函数基本上是工程项目的常客. 尤其是在日志函数/日志类的实现部分. C/C++语言本身并无代码文件的基本信息原语(function, fileline, filename), 而这些信息是调试的必备, 有了出错处的函数名,对应代码行, 程序员基本上就能锁定第一现场. 但因为没有原语, 这里只能请出宏来帮忙.

1
/// leveldb/db/c_test.c

#define CheckNoError(err)                                               \
  if ((err) != NULL) {                                                  \
    fprintf(stderr, "%s:%d: %s: %s\n", __FILE__, __LINE__, phase, (err)); \
    abort();                                                            \
  }

#define CheckCondition(cond)                                            \
  if (!(cond)) {                                                        \
    fprintf(stderr, "%s:%d: %s: %s\n", __FILE__, __LINE__, phase, #cond); \
    abort();                                                            \
  }


int main(int argc, char** argv) {
  // ... some code

  StartPhase("leveldb_free");
  db = leveldb_open(options, dbname, &err);
  CheckCondition(err != NULL);
  leveldb_free(err);
  err = NULL;

  StartPhase("open");
  leveldb_options_set_create_if_missing(options, 1);
  db = leveldb_open(options, dbname, &err);
  CheckNoError(err);
  CheckGet(db, roptions, "foo", NULL);
  
  // ... some code
}

以上片段是leveldb里的一个测试代码片段, 其中用到宏函数来进行assert判定, 这里也是没办法用普通函数替换的, 原因是其中的FILE, LINE正是我们需要得到的输出信息, 而若用普通函数的话, 这里的FILE, LINE就不能显示正确了.

其实还有很多地方, 宏函数都可以给我们露一手, 演出惊艳一幕. 在有大量相似代码时, 不妨考虑使用宏函数, 清爽一下代码. 在有普通函数搞不定或者太繁琐时, 也不妨考虑下宏函数, 说不定就能手到擒来.

宏, C/C++基本语法和template构成C/C++语系的三大基石. 我们阅读各种经典项目时都能发现宏的身影. 可惜的是, 自教学之始, 到各种大佬大师的口诛笔伐, 宏在C++ coder的世界里始终是三等公民, 甚至被无条件排斥. 不可否认, 宏确实是劣质代码的罪首之一, 宏定义重复, 宏覆盖, 宏展开异常等等带来各种难以调试的bug. 但若我们能正确使用, 宏不乏其可爱之处, 并不是那么坏的存在.

参考

http://www.chinaunix.net/old_jh/23/408225.html