把 C/C++ 的变参宏玩出花样
0. 背景
本文描述的技巧是我在实现 ToplingDB 的 Enum Reflection 时,开发的一个技巧,其核心是获取变参宏参数列表的长度(这个是在网上看到的,讲述该技巧的文章很多),以此为机制,构建了一个方法体系。在此与大家分享。
1. 引言
- C 语言有变参函数,例如
printf
- C++11 引入了变参模板 (variadic template)
相应的,在 C 预处理器中,其实也很早就支持“变参宏”了,例如:
#define MY_LOG(level, fmt, ...) \
if (level > g_level) printf(fmt, ##__VA_ARGS__)
2. 问题
2.1. 参数变换
前述的 MY_LOG
只是简单地把自己的参数原封不动地“转发”给 printf
,那么,我们能否对参数做一些变换,再转发给 printf 呢?例如,对于 std::string
,我们转发它的 .c_str()
。
#define SmartPrintf(fmt,...) some impl ...
我们期望:
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
可以等效于:
printf("DB::Open(%s) fail with status code = %d, msg = %s\n",
dbname.c_str(), status.code(), status.ToString().c_str());
2.2. 默认参数
例如系统调用 open
:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
其实它的函数原型是:
int open(const char *pathname, int flags, ...);
mode
参数只有在创建文件时才有用,往往有人在创建文件时忘记传入第三个参数而导致文件 mode 成为一个莫名其妙的值(UB : undefined behavior)。
那么,我们能否在 C 语言的能力范围内,定义一个宏 SafeOpen
转调 open
,即便传两个参数时,也不会 UB ?
2.3. 超级能力
例如 Enum Reflection,将宏的参数使用多次,并且每次都展开为不同的形式。
3. 解决方案
第一步,我们得知道,preproc 的能力边界在哪里,一切都必须在这个能力边界内运转。
3.1. 利用变参宏的能力
#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_X
有 M+2
个固定参数,再加一个可变参数列表,其展开为固定参数列表的最后一个参数 XX。 当通过 PP_ARG_N
给 PP_ARG_X
传递的变参列表 __VA_ARGS__
代表的参数列表长度为 N 时,PP_ARG_X
的参数 XX 将展开为 N,于是我们就得到了 __VA_ARGS__
变参列表的长度。
现在,我们再定义一个实用宏 PP_VA_NAME
:
#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,即:如果我们定义了一系列宏或函数:
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__)
那么:
\
再继续:
以这样的方式,我们可以在 C 语言的语法范围内,实现 C++ 中仅根据参数个数的 overload ,于是,我们可接解决系统调用 open
创建文件时误传 2 个参数的问题了:
// 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,首先,我们实现一个参数变换宏:
#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),假定我们有个函数:
int map(void* context,int);
现在,我们可以实现 SmartPrint
了:
3.2. SmartPrintf (必须使用 C++)
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, SmartData, __VA_ARGS__)
#define SmartPrintf(fmt, ...) printf(fmt, PP_SmartList(__VA_ARGS__))
4. 使用 C++,玩出花样
ToplingDB Enum Reflection 的实现就使用了这一系列技巧。
topling-zip 中也充分利用了这些技巧,例如,我们可以这样使用:
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 排序,排序规则是:
- 先按 str 字典序从小到大
- 如果 str 字段相同,再按 num 从大到小
- 如果 str 和 num 都相同,再按 score 从大到小
5. ToplingDB 的更多内容:
- ToplingDB 使用入门
- ToplingDB SidePlugin 配置系统
- ToplingDB 的 旁路插件化
- ToplingDB 使用 REST Web 在线修改配置
- CompactionFilterFactory As SidePlugin
- ToplingDB 省略 L0 Flush
- 如何评价 ToplingDB 的内嵌 Web?
- ToplingDB 和 TerarkDB 有什么区别?
使用了 ToplingDB 的 Todis(外存版 Redis):
转载自:https://juejin.cn/post/7133052516794646564