likes
comments
collection
share

把 C/C++ 的变参宏玩出花样

作者站长头像
站长
· 阅读数 40

原文链接:Variadic Preproc · topling/rockside Wiki (github.com)

0. 背景

本文描述的技巧是我在实现 ToplingDB 的 Enum Reflection 时,开发的一个技巧,其核心是获取变参宏参数列表的长度(这个是在网上看到的,讲述该技巧的文章很多),以此为机制,构建了一个方法体系。在此与大家分享。

1. 引言

  1. C 语言有变参函数,例如 printf
  2. 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++ 的变参宏玩出花样

\

再继续:

把 C/C++ 的变参宏玩出花样

以这样的方式,我们可以在 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);

把 C/C++ 的变参宏玩出花样

现在,我们可以实现 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__))

把 C/C++ 的变参宏玩出花样

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 排序,排序规则是:

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

5. ToplingDB 的更多内容:

  1. ToplingDB 使用入门
  2. ToplingDB SidePlugin 配置系统
  3. ToplingDB 的 旁路插件化
  4. ToplingDB 使用 REST Web 在线修改配置
  5. CompactionFilterFactory As SidePlugin
  6. ToplingDB 省略 L0 Flush
  7. 如何评价 ToplingDB 的内嵌 Web?
  8. ToplingDB 和 TerarkDB 有什么区别?

使用了 ToplingDB 的 Todis(外存版 Redis):

  1. Todis 中分布式 Compact 是怎么工作的?
  2. Todis的性能那么强,真是因为分布式Compact吗?
  3. ToplingDB的分布式Compact和RocksDB的RemoteCompaction有什么不同?
  4. 如何看待 Todis 的数据存储编码格式?