Steam 回调宏
在接入 Steam SDK 时在 demo 中看到了一个宏,用来接受 Steam 的相关回调,没有明白时如何实现的。宏定义如下:
1 | class CStatsAndAchievements |
其中的 STEAM_CALLBACK
是一个宏,查看它的定义是这样的:
1 | // steam_api_common.h |
注释说明了这个宏的作用:声明一个 callback
类型的成员方法和一个辅助成员变量,用来在这个类对象创建时注册回调和销毁时移除注册。
后面又多了一个宏 _STEAM_CALLBACK_SELECT
,看到这个宏,产生了一堆问题。这个宏是如何做到定义成员变量和方法的?宏定义中的 ...
和 __VA_ARGS
分别又是什么?其中的 4, 3
什么含义?为什么中间有个逗号?甚至还有一个 ( /**/, thisclass
被忽略的参数?
继续查看 _STEAM_CALLBACK_SELECT
的定义:
1 | // steam_api_internal.h |
又多了一堆宏定义,可谓是连环套一般的宏。
展开分析
宏定义需要一步一步展开分析,先从第一步开始:
STEAM_CALLBACK
宏
CStatsAndAchievements
类中的宏
1 | STEAM_CALLBACK( CStatsAndAchievements, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived ); |
根据宏定义
1 |
|
其中的...
和 __VA_ARGS__
表示可变参数宏,在定义宏的时候加上 ...
表示一个或多个参数。在宏展开式通过 __VA_ARGS__
来替换前面的 ...
。第一行反斜杠 \
代表换行,宏定义一行放不下,换到下一行。
宏的各个参数
1 | thisclass -> CStatsAndAchievements |
上述宏展开为
1 | _STEAM_CALLBACK_SELECT( (UserStatsReceived_t, m_CallbackUserStatsReceived, 4, 3), (, CStatsAndAchievements, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived)); |
继续展开
_STEAM_CALLBACK_SELECT
宏
根据宏定义
1 |
其中的各个参数
1 | X -> (UserStatsReceived_t, m_CallbackUserStatsReceived, 4, 3) |
因为宏是文本替换,所以包含参数中的括号。上述宏展开为
1 | _STEAM_CALLBACK_HELPER (UserStatsReceived_t, m_CallbackUserStatsReceived, 4, 3) (, CStatsAndAchievements, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived); |
注意其中两个圆括号之间是没有逗号的,后面的括号里有一个单独的逗号。这样分成了两部分,前面是 _STEAM_CALLBACK_HELPER
。
_STEAM_CALLBACK_HELPER
宏
根据宏定义
1 |
其中的 ##
表示 Token 连接(Token Pasting Operator),即将两个值的内容拼成一个值。
上述宏前面的部分
1 | _STEAM_CALLBACK_HELPER (UserStatsReceived_t, m_CallbackUserStatsReceived, 4, 3) |
其中的各个参数为
1 | _1 -> UserStatsReceived_t |
所以上述宏被替换为
1 | _STEAM_CALLBACK_4 |
加上后面的部分,完整的展开为
1 | _STEAM_CALLBACK_4 (, CStatsAndAchievements, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived); |
可以看出 _STEAM_CALLBACK_HELPER
的参数都被丢弃掉了,只是为了替换成 _STEAM_CALLBACK_4
这个宏。这样做是出于什么考虑呢,后面会解释。
继续展开
_STEAM_CALLBACK_4
宏
根据其定义
1 |
|
其中的参数
1 | _ -> // 空 |
最终宏展开为
1 | CCallback< CStatsAndAchievements, UserStatsReceived_t> m_CallbackUserStatsReceived; void OnUserStatsReceived(UserStatsReceived_t *pParam); |
由此可以看出定义了一个 CCallback
类型的成员变量 m_CallbackUserStatsReceived
,和一个 OnUserStatsReceived
方法。
查阅文档可知 m_CallbackUserStatsReceived
是负责在对象创建时将回调方法 OnUserStatsReceived
注册到 Steam,并在对象销毁时移除注册。回调方法 OnUserStatsReceived
方法接收一个 UserStatsReceived_t *
类型的参数。
_STEAM_CALLBACK_HELPER 宏
上面说到这个宏会被替换为 _STEAM_CALLBACK_4
,只用到了一个参数,其他的参数都被丢弃掉了。这里的用途是为什么呢。
经过查阅发现是一种重载(Overload)宏的方式,使一个宏可以根据参数数量的不同来做不同的展开。函数方法中经常会有重载的例子,比如:
1 | void func(int x); |
但是如果定义了两个同名的宏,前面的定义就会被后面的定义所覆盖。比如有两个宏定义:
1 |
最终只会用到下面的宏定义,接受 3 个参数。
解决方案是利用了可变参数宏,比如定义两个宏:
1 |
再定义一个帮助宏:
1 |
这样使用 MY_MACRO
宏时如果是两个参数,依次展开如下:
1 | MY_MACRO(a, b) |
如果三个参数 MY_MACRO(a, b, c)
,依次也可以最终展开为 MY_MACRO_3(a, b, c)
。最终就达到了重载宏定义的目的。
这样也解释了为什么 Steam 中的宏
1 | _STEAM_CALLBACK_HELPER (UserStatsReceived_t, m_CallbackUserStatsReceived, 4, 3) |
中使用到的数字 4, 3
,以及为什么参数都被丢弃了。是因为有两个宏 _STEAM_CALLBACK_4
和 _STEAM_CALLBACK_3
。会根据参数数目的不同来使用不同的宏。前面的参数是为了占位用,所以后面会被丢弃。
参考链接
- 整理:C/C++可变参数,“## VA_ARGS”宏的介绍和使用,bat67。
- 可变参数宏,zh.wikipedia.org。
- C++ preprocessor VA_ARGS number of arguments,stackoverflow.com。
- C预处理器#Token连接,zh.wikipedia.org。
- define宏定义中的#,##,@#及/符号,xdsoft365。
- Overloading Macro on Number of Arguments,stackoverflow.com。