Variadic-Preproc

1. 引言

  1. C 语言有变参函数,例如 printf
  2. C++11 引入了变参模板 (variadic template)

相应的,在 C 预处理器中,其实也很早就支持“变参宏”了,例如:

1
2
#define MY_LOG(level, fmt, ...) \
if (level > g_level) printf(fmt, ##__VA_ARGS__)

2. 问题

2.1. 参数变换

前述的 MY_LOG 只是简单地把自己的参数原封不动地“转发”给 printf,那么,我们能否对参数做一些变换,再转发给 printf 呢?例如,对于 std::string,我们转发它的 .c_str()

1
#define SmartPrintf(fmt,...) some impl ...

我们期望:

1
2
3
4
5
std::string dbname = ...;
Status status = DB::Open(dbname, ...);
if (!status.ok())
SmartPrintf("DB::Open(%s) fail with status code = %d, msg = %s\n",
dbname, status.code(), status.ToString());

其中 SmartPrintf 可以等效于:

1
2
printf("DB::Open(%s) fail with status code = %d, msg = %s\n",
dbname.c_str(), status.code(), status.ToString().c_str());

2.2. 默认参数

例如系统调用 open:

1
2
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

其实它的函数原型是:

1
int open(const char *pathname, int flags, ...);

mode 参数只有在创建文件时才有用,往往有人在创建文件时忘记传入第三个参数而导致文件 mode 成为一个莫名其妙的值(UB : undefined behavior)。

那么,我们能否在 C 语言的能力范围内,定义一个宏 SafeOpen 转调 open,即便传两个参数时,也不会 UB ?

2.3. 超级能力

例如 [[Enum Reflection]],将宏的参数使用多次,并且每次都展开为不同的形式。

3. 解决方案

第一步,我们得知道,preproc 的能力边界在哪里,一切都必须在这个能力边界内运转。

3.1. 利用变参宏的能力

1
2
3
4
5
6
7
8
#define PP_ARG_X(_0,_1,_2,_3,_4,_5,_6,_7,_8,_9, \
a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z, \
A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,XX,...) XX
#define PP_ARG_N(...) \
PP_ARG_X("ignored", ##__VA_ARGS__, \
Z,Y,X,W,V,U,T,S,R,Q,P,O,N,M,L,K,J,I,H,G,F,E,D,C,B,A, \
z,y,x,w,v,u,t,s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a, \
9,8,7,6,5,4,3,2,1,0)

PP_ARG_N(...) 会展开为该宏调用中参数的个数,它利用 PP_ARG_X 宏作为辅助,PP_ARG_XM+2 个固定参数,再加一个可变参数列表,其展开为固定参数列表的最后一个参数 XX。
当通过 PP_ARG_NPP_ARG_X 传递的变参列表 __VA_ARGS__ 代表的参数列表长度为 N 时,PP_ARG_X 的参数 XX 将展开为 N,于是我们就得到了 __VA_ARGS__ 变参列表的长度。

现在,我们再定义一个实用宏 PP_VA_NAME

1
2
3
4
#define PP_VA_NAME(prefix,...) \
PP_CAT2(prefix,PP_ARG_N(__VA_ARGS__))
#define PP_CAT2(a,b) PP_CAT2_1(a,b)
#define PP_CAT2_1(a,b) a##b

该宏用来作为 dispatch,即:如果我们定义了一系列函数:

1
2
3
4
5
6
void func_0();
void func_1(int);
void func_2(int,int);
void func_2(int,int,int);
// more func_N ...
#define func(...) PP_VA_NAME(func_,__VA_ARGS__)(__VA_ARGS__)

那么:

宏调用 宏展开
PP_VA_NAME(func_) func_0
PP_VA_NAME(func_, a) func_1
PP_VA_NAME(func_, a, b) func_2
PP_VA_NAME(func_, a, b, c) func_3

再继续:

宏调用 宏展开(中间形式) 宏展开(最终)
func() PP_VA_NAME(func_)() func_0()
func(a) PP_VA_NAME(func_, a)(a) func_1(a)
func(a, b) PP_VA_NAME(func_, a, b)(a, b) func_2(a, b)
func(a, b, c) PP_VA_NAME(func_, a, b, c)(a, b, c) func_3(a, b, c)

以这样的方式,我们可以在 C 语言的语法范围内,实现 C++ 中仅根据参数个数的 overload ,于是,我们可接解决系统调用 open 创建文件时误传 2 个参数的问题了:

1
2
3
4
5
6
7
8
// default mode = 0600
#define SafeOpen_2(pathname, flags) open(pathname, flags, 0600)
#define SafeOpen_3(pathname, flags, mode) open(pathname, flags, mode)
#define SafeOpen(...) PP_VA_NAME(SafeOpen_, __VA_ARGS__)(__VA_ARGS__)
// is equivalent to C++:
inline int SafeOpen(const char* pathname, int flags, int mode = 0600) {
return open(pathname, flags, mode);
}

3.2. 参数变换

现在,我们来实现一个在 C++ 中使用起来更方便的 printf,首先,我们实现一个参数变换:

1
2
3
4
5
6
7
8
9
10
#define PP_MAP_0(m,c)
#define PP_MAP_1(m,c,x) m(c,x)
#define PP_MAP_2(m,c,x,y) m(c,x),m(c,y)
#define PP_MAP_3(m,c,x,y,z) m(c,x),m(c,y),m(c,z)
#define PP_MAP_4(m,c,x,...) m(c,x),PP_MAP_3(m,c,__VA_ARGS__)
#define PP_MAP_5(m,c,x,...) m(c,x),PP_MAP_4(m,c,__VA_ARGS__)
// more PP_MAP_...

#define PP_MAP(map,ctx,...) \
PP_VA_NAME(PP_MAP_,__VA_ARGS__)(map,ctx,##__VA_ARGS__)

这个 PP_MAP 把每个宏参数 x 变换成 m(c,x),假定我们有个函数:

1
int map(void* context,int);
宏调用 宏展开
PP_MAP(map, ctx, a) map(ctx, a)
PP_MAP(map, ctx, a, b) map(ctx, a), map(ctx, a, b)
PP_MAP(map, ctx, a, b, c) map(ctx, a), map(ctx, a, b), map(ctx, a, c)

现在,我们可以实现 SmartPrint 了:

3.2. SmartPrintf (必须使用 C++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<class T>
inline typename std::enable_if<std::is_fundamental<T>::value, T>::type
SmartData(T x) { return x; }

template<class Seq>
inline auto
SmartData(const Seq& s) -> decltype(s.data()) { return s.data(); }

template<class StdException>
inline auto
SmartData(const StdException& e) -> decltype(e.what()) { return e.what(); }

template<class T>
inline const T* SmartData(const T* x) { return x; }

#define PP_SmartList(...) \
PP_MAP(PP_APPLY, SmartDataForPrintf, __VA_ARGS__)

#define SmartPrintf(fmt, ...) printf(fmt, PP_SmartList(__VA_ARGS__))
宏调用 宏展开
SmartPrintf(“str=%s\n”, str) printf(“num=%s\n”, SmartData(str))
SmartPrintf(“str=%s, num=%d\n”, str, num) printf(“str=%s, num=%d\n”, SmartData(str), SmartData(num))

4. 使用 C++,玩出花样

ToplingDB Enum Reflection 的实现就使用了这一系列技巧。

topling-zip 中也充分利用了这些技巧,例如,我们可以这样使用:

1
2
3
4
5
6
7
8
9
10
struct MyData {
string str;
int num;
double score;
// more ...
};
vector<MyData> vec;
// read data to vec
auto beg = vec.begin(), end = vec.end();
sort(beg, end, TERARK_CMP(str, <, num, >, score, >));

这个代码就很直观了,对 vec 排序,排序规则是:

  1. 先按 str 字典序从小到大
  2. 如果 str 字段相同,再按 num 从大到小
  3. 如果 str 和 num 都相同,再按 score 从大到小