宏编程基础
  ZXL8ALkrtBPG 2023年11月02日 65 0
cpp

C++中通过宏编程配合模板元编程,可以在编译期实现很多有趣的设计;同时C++的宏编程是图灵完备的,本文介绍在平时编码中经常使用的一些宏编程特性。

宏展开规则

C++标准n4296中的第16章对宏编程预处理进行详细说明【1】;

#define EXPAND_PARAM(arg1, arg2) arg1, arg2   

EXPAND_PARAM(int , float) // 类似函数调用

宏调用类似于函数调用,参数于函数参数相同,以逗号进行分隔;

如果宏参数为另一个宏,和函数调用时一样,宏代码在预处理扫描过程中会将参数中的宏先展开:

template<typename ...Ts>
void Fun(Ts&&...ts)   
{
    int a[] = {(std::cout << ts << ", ", 0)...};
    std::cout << std::endl;
}
    
int main()
{
    Fun(EXPAND_PARAM(EXPAND_PARAM(1, 2), EXPAND_PARAM(3, 4)));
}

// 输出:1,2,3,4,

展开顺序为:

宏编程基础_cpp

这里经过两次扫描得到宏展开结果;

宏展开以逗号分隔

以下面代码为例:

template<typename ...Ts>
struct TestClass {
};

#define TEST_CLASS(stream, ...) TestClass<std::tuple<stream>, __VA_ARGS__>;

template<typename ...Ts>
struct MyClass {
    using Type = TEST_CLASS(Ts..., float, double);
};

int main()
{
    using Type = MyClass<char, int>;
}

“Ts..., float, double”宏参数以逗号为分隔,因此第一个宏参数stream对应的为Ts...,即为char,int,因此Type对应的类型为TestClass, float, double>;

#和##操作符

# 操作符后面跟的宏参数转换为字符串,修改上述例子:

#define STR(...) #__VA_ARGS__

#define EXPAND_PARAM(arg1, arg2) arg1, arg2

#define IDENTITY(arg0) arg0

template<typename ...Ts>
void Fun(Ts&&...ts)
{
    int a[] = {(std::cout << ts << ", ", 0)...};
    std::cout << std::endl; 
}

int main()
{
    Fun(EXPAND_PARAM(IDENTITY(0), EXPAND_PARAM(3, 4)));
    Fun(EXPAND_PARAM(STR(IDENTITY(0)), EXPAND_PARAM(3, 4)));
}
0, 3, 4, 
IDENTITY(0), 3, 4,

可以看到在添加了STR后,IDENTITY(0)宏并没有展开,而直接作为字符串输出,这里的规则为:

如果宏参数在下一次预处理时是作为#的宏参数,则宏参数不会展开,可以通过将#延迟到下一次展开解决:

#define _STR(...) #__VA_ARGS__
#define STR(...) _STR(__VA_ARGS__)
Fun(EXPAND_PARAM(STR(IDENTITY(0)), EXPAND_PARAM(3, 4)));
 // 输出:0, 3, 4,

##操作符用于前后宏参数的拼接,例如:

#define CONCAT(arg0, arg1) arg0 ## arg1
Fun(CONCAT(1, 1), STR(CONCAT(Hello, World)));
//输出:11, HelloWorld,

修改添加:

Fun(CONCAT(1, 1), STR(CONCAT(Hello, IDENTITY(World))));
// 输出:11, HelloIDENTITY(World),

通用IDENTITY(world)遇到##没有展开,因此相同的方法修改则可以正常展开,不在赘述;

#define _CONCAT(arg0, arg1) arg0 ## arg1
#define CONCAT(arg0, arg1) _CONCAT(arg0, arg1)

总结: 一次宏展开先把参数完全展开,遇到 ###则不会展开;展开之后再进行扫描继续展开宏直到没有可展开的宏;

禁止递归重入

基于上一节中的代码继续修改:

Fun(STR(CONCAT(CON, CAT(Hello, World))));
输出:CONCAT(Hello, World),

通过CONCAT拼接得到CONCAT没有进行二次展开,这里可以理解为递归过程看做一颗数,每次展开会检查父节点是否已经展开过,如果已经标记为展开过,则会禁止递归展开同一个宏;

变长参数

宏可以接受变长参数,例如在实现日志打印时,经常会使用:

#define DBG_LOG(fmt, ...) \
    __LOG_TITLE(debug, "[%s:%d] " fmt" ", __FUNCTION__, __LINE__, ##__VA_ARGS__)

其中##则为避免变长参数为空时,使预处理器(preprocessor)去除掉它前面的那个逗号;

获取宏参数个数

可以在编译期获取宏参数的个数,实现如下效果:

static_assert(GET_ARG_COUNT() == 0); 
    static_assert(GET_ARG_COUNT(1, 2, 3) == 3); 
    static_assert(GET_ARG_COUNT(1, 2, 3, 4, 5, 6) == 6); 
    static_assert(GET_ARG_COUNT(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) == 10);

注意这里GET_ARG_COUNT是一个宏,这里提供一个参考实现:

#define GET_ARG_COUNT(...)                                                 \
    INTERNAL_GET_ARG_COUNT(                                                \
        unused, ##__VA_ARGS__, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90,  \
        89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74,    \
        73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58,    \
        57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42,    \
        41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26,    \
        25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10,    \
        9,  8, 7, 6, 5, 4, 3, 2, 1, 0)
    
#define INTERNAL_GET_ARG_COUNT(                                            \
    e0, e1, e2, e3, e4, e5, e6, e7, e8, e9, e10, e11, e12, e13, e14, e15,  \
    e16, e17, e18, e19, e20, e21, e22, e23, e24, e25, e26, e27, e28, e29,  \
    e30, e31, e32, e33, e34, e35, e36, e37, e38, e39, e40, e41, e42, e43,  \
    e44, e45, e46, e47, e48, e49, e50, e51, e52, e53, e54, e55, e56, e57,  \
    e58, e59, e60, e61, e62, e63, e64, e65, e66, e67, e68, e69, e70, e71,  \
    e72, e73, e74, e75, e76, e77, e78, e79, e80, e81, e82, e83, e84, e85,  \
    e86, e87, e88, e89, e90, e91, e92, e93, e94, e95, e96, e97, e98, e99,  \
    e100, count, ...)                                                      \
    count

当GET_ARG_COUNT()进程宏参数替换时得到:

INTERNAL_GET_ARG_COUNT(                                            \
        unused,100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90,  \
        89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74,    \
        73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58,    \
        57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42,    \
        41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26,    \
        25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10,    \
        9,  8, 7, 6, 5, 4, 3, 2, 1, count, ...)                                         
    count

此时count即为0,“...” 为空,同理当使用GET_ARG_COUNT(1,2,3,4,5,6,7,8,9,10),“...” 为“9,8,7,6,5,4,3,2,1”,count则对应为10;

逻辑运算

参考【3】的代码实现进行分析:

#define PRIMITIVE_CAT(a, ...) a ## __VA_ARGS__   

#define CHECK_N(x, n, ...) n
#define CHECK(...) CHECK_N(__VA_ARGS__, 0,)
#define PROBE(x) x, 1,

#define NOT(x) CHECK(PRIMITIVE_CAT(NOT_, x))
#define NOT_0 PROBE(~)

#define COMPL(b) PRIMITIVE_CAT(COMPL_, b)
#define COMPL_0 1
#define COMPL_1 0

#define BOOL(x) COMPL(NOT(x))

则BOOL(0) 输出为0,其他非0值输出为1,分析宏展开流程如下:

宏编程基础_cpp_02

条件选择

#define IIF(c) PRIMITIVE_CAT(IIF_, c)     
#define IIF_0(t, ...) __VA_ARGS__     
#define IIF_1(t, ...) t     
    
#define IF(c) IIF(BOOL(c))

配合逻辑判断,选择对应的宏参数;

遍历访问参数

结合上面已经介绍的方案,然后进一步想对每个参数进行处理,这里以10个参数为例来说明实现原理,更多参数同理进行扩展即可,Demo代码如下:

#define REPEAT_0(func, i, arg)
#define REPEAT_1(func, i, arg)        func(1, arg)
#define REPEAT_2(func, i, arg, ...)   func(2, arg) REPEAT_1(func, i + 1, __VA_ARGS__)
#define REPEAT_3(func, i, arg, ...)   func(3, arg) REPEAT_2(func, i + 1, __VA_ARGS__)
#define REPEAT_4(func, i, arg, ...)   func(4, arg) REPEAT_3(func, i + 1, __VA_ARGS__)
#define REPEAT_5(func, i, arg, ...)   func(5, arg) REPEAT_4(func, i + 1, __VA_ARGS__)
#define REPEAT_6(func, i, arg, ...)   func(6, arg) REPEAT_5(func, i + 1, __VA_ARGS__)
#define REPEAT_7(func, i, arg, ...)   func(7, arg) REPEAT_6(func, i + 1, __VA_ARGS__)
#define REPEAT_8(func, i, arg, ...)   func(8, arg) REPEAT_7(func, i + 1, __VA_ARGS__)
#define REPEAT_9(func, i, arg, ...)   func(9, arg) REPEAT_8(func, i + 1, __VA_ARGS__)
#define REPEAT_10(func, i, arg, ...)  func(10, arg) REPEAT_9(func, i + 1, __VA_ARGS__)

// 自减
#define DEC(arg0) CONCAT(DEC_, arg0)
#define DEC_0 0
#define DEC_1 0
#define DEC_2 1
#define DEC_3 2
#define DEC_4 3
#define DEC_5 4
#define DEC_6 5
#define DEC_7 6
#define DEC_8 7
#define DEC_9 8
#define DEC_10 9

// 逻辑运算
#define PRIMITIVE_CAT(a, ...) a ## __VA_ARGS__

#define CHECK_N(x, n, ...) n
#define CHECK(...) CHECK_N(__VA_ARGS__, 0,)
#define PROBE(x) x, 1,

#define NOT(x) CHECK(PRIMITIVE_CAT(NOT_, x))
#define NOT_0 PROBE(~)

#define COMPL(b) PRIMITIVE_CAT(COMPL_, b)
#define COMPL_0 1
#define COMPL_1 0

#define BOOL(x) COMPL(NOT(x))

// 条件判断 
#define IIF(c) PRIMITIVE_CAT(IIF_, c)
#define IIF_0(t, ...) __VA_ARGS__
#define IIF_1(t, ...) t
#define IF(c) IIF(BOOL(c))

#define EAT(...)
// 添加逗号分隔符
#define COMMA() ,
#define COMMA_IF(n) IF(n)(COMMA, EAT)()
template<typename ...Ts>
class TestClass {};
template<typename ...Ts>
struct TypeWrapper {};

#define _TYPE(index, ARG) TypeWrapper<ARG> COMMA_IF(DEC(index))
#define TYPE_WRAPPER(...) CONCAT(REPEAT_, GET_ARG_COUNT(__VA_ARGS__))(_TYPE, GET_ARG_COUNT(__VA_ARGS__), __VA_ARGS__)

int main()
{
    using Type = TestClass<TYPE_WRAPPER(char, int, float, double)>;
    std::cout << "T type:" << type_id_with_cvr<Type>().pretty_name() << std::endl;
}

如果遍历可以将所有宏参数转换为TypeWrapper<T>,即在编译期将char, int, float, double通过宏参数遍历为:

TypeWrapper<char>, TypeWrapper<int>, TypeWrapper<float>, TypeWrapper<double>

其中COMMA_IF通过判断是否为最后一个宏参数来决定是否添加逗号分隔符;

条件循环

while (pred(val)) {
    val = op(val);
}

在【3】提供了如下实现:

#define DEFER(id) id EMPTY()
#define OBSTRUCT(id) id DEFER(EMPTY)()

#define EVAL(...)  EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
#define EVAL1(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
#define EVAL2(...) EVAL3(EVAL3(EVAL3(__VA_ARGS__)))
#define EVAL3(...) EVAL4(EVAL4(EVAL4(__VA_ARGS__)))
#define EVAL4(...) EVAL5(EVAL5(EVAL5(__VA_ARGS__)))
#define EVAL5(...) __VA_ARGS__

#define WHILE(pred, op, ...) \
    IF(pred(__VA_ARGS__)) \
    ( \
        OBSTRUCT(WHILE_INDIRECT) () \
        ( \
            pred, op, op(__VA_ARGS__) \
        ), \
        __VA_ARGS__ \
    )
#define WHILE_INDIRECT() WHILE

这里应用宏延迟展开的技巧,读者可参考【2】;

#define PRED(state, ...) BOOL(state)
#define OP(state, ...) DEC(state), state, __VA_ARGS__
#define MACRO(state) state

EVAL(WHILE(PRED, OP, 8,))
// 输出:0, 1, 2, 3, 4, 5, 6, 7, 8,

分析展开流程为:

宏编程基础_cpp_03

参考文献

【1】https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4296.pdf

【2】https://zhuanlan.zhihu.com/p/27146532?from_voters_page=true

【3】https://github.com/pfultz2/Cloak

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

上一篇: c语言 下一篇: Golang源码学习之heap
  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  ZXL8ALkrtBPG   2023年11月02日   47   0   0 cpp
  PQYWJLBjS0G7   2023年11月02日   47   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   66   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   38   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   39   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   46   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   76   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   31   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   26   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   33   0   0 cpp
  ZXL8ALkrtBPG   2023年11月19日   18   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   54   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   68   0   0 Listcpp
  ZXL8ALkrtBPG   2023年11月02日   46   0   0 cpp
  ZXL8ALkrtBPG   2023年11月02日   66   0   0 cpp
  ZXL8ALkrtBPG   2023年11月19日   31   0   0 cpp
ZXL8ALkrtBPG