0%

Steam SDK demo 中的一个有意思的 C++ 宏

Steam 回调宏

在接入 Steam SDK 时在 demo 中看到了一个宏,用来接受 Steam 的相关回调,没有明白时如何实现的。宏定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CStatsAndAchievements
{
public:
// ...省略
// 类中的定义,使用了宏定义 STEAM_CALLBACK
STEAM_CALLBACK( CStatsAndAchievements, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived );
// ...省略
}
// 构造函数
CStatsAndAchievements::CStatsAndAchievements( IGameEngine *pGameEngine )
:
// ...省略
m_CallbackUserStatsReceived( this, &CStatsAndAchievements::OnUserStatsReceived )
// ...省略
{}
// 接收回调方法
void CStatsAndAchievements::OnUserStatsReceived( UserStatsReceived_t *pCallback ) {}

其中的 STEAM_CALLBACK 是一个宏,查看它的定义是这样的:

1
2
3
4
5
6
7
// steam_api_common.h
// Declares a callback member function plus a helper member variable which
// registers the callback on object creation and unregisters on destruction.
// The optional fourth 'var' param exists only for backwards-compatibility
// and can be ignored.
#define STEAM_CALLBACK( thisclass, func, .../*callback_type, [deprecated] var*/ ) \
_STEAM_CALLBACK_SELECT( ( __VA_ARGS__, 4, 3 ), ( /**/, thisclass, func, __VA_ARGS__ ) )

注释说明了这个宏的作用:声明一个 callback 类型的成员方法和一个辅助成员变量,用来在这个类对象创建时注册回调和销毁时移除注册。

后面又多了一个宏 _STEAM_CALLBACK_SELECT,看到这个宏,产生了一堆问题。这个宏是如何做到定义成员变量和方法的?宏定义中的 ...__VA_ARGS 分别又是什么?其中的 4, 3 什么含义?为什么中间有个逗号?甚至还有一个 ( /**/, thisclass 被忽略的参数?

继续查看 _STEAM_CALLBACK_SELECT 的定义:

1
2
3
4
5
// steam_api_internal.h
#define _STEAM_CALLBACK_HELPER( _1, _2, SELECTED, ... ) _STEAM_CALLBACK_##SELECTED
#define _STEAM_CALLBACK_SELECT( X, Y ) _STEAM_CALLBACK_HELPER X Y
#define _STEAM_CALLBACK_4( _, thisclass, func, param, var ) \
CCallback< thisclass, param > var; void func( param *pParam )

又多了一堆宏定义,可谓是连环套一般的宏。

展开分析

宏定义需要一步一步展开分析,先从第一步开始:

STEAM_CALLBACK

CStatsAndAchievements 类中的宏

1
STEAM_CALLBACK( CStatsAndAchievements, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived );

根据宏定义

1
2
#define STEAM_CALLBACK( thisclass, func, .../*callback_type, [deprecated] var*/ ) \
_STEAM_CALLBACK_SELECT( ( __VA_ARGS__, 4, 3 ), ( /**/, thisclass, func, __VA_ARGS__ ) )

其中的...__VA_ARGS__ 表示可变参数宏,在定义宏的时候加上 ... 表示一个或多个参数。在宏展开式通过 __VA_ARGS__ 来替换前面的 ...。第一行反斜杠 \ 代表换行,宏定义一行放不下,换到下一行。

宏的各个参数

1
2
3
thisclass    -> CStatsAndAchievements
func -> OnUserStatsReceived
... -> UserStatsReceived_t, m_CallbackUserStatsReceived

上述宏展开为

1
_STEAM_CALLBACK_SELECT( (UserStatsReceived_t, m_CallbackUserStatsReceived, 4, 3), (, CStatsAndAchievements, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived));

继续展开

_STEAM_CALLBACK_SELECT

根据宏定义

1
#define _STEAM_CALLBACK_SELECT( X, Y )		_STEAM_CALLBACK_HELPER X Y

其中的各个参数

1
2
X  -> (UserStatsReceived_t, m_CallbackUserStatsReceived, 4, 3)
Y -> (, CStatsAndAchievements, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived)

因为宏是文本替换,所以包含参数中的括号。上述宏展开为

1
_STEAM_CALLBACK_HELPER (UserStatsReceived_t, m_CallbackUserStatsReceived, 4, 3) (, CStatsAndAchievements, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived);

注意其中两个圆括号之间是没有逗号的,后面的括号里有一个单独的逗号。这样分成了两部分,前面是 _STEAM_CALLBACK_HELPER

_STEAM_CALLBACK_HELPER

根据宏定义

1
#define _STEAM_CALLBACK_HELPER( _1, _2, SELECTED, ... )		_STEAM_CALLBACK_##SELECTED

其中的 ## 表示 Token 连接(Token Pasting Operator),即将两个值的内容拼成一个值。

上述宏前面的部分

1
_STEAM_CALLBACK_HELPER (UserStatsReceived_t, m_CallbackUserStatsReceived, 4, 3)

其中的各个参数为

1
2
3
4
_1          -> UserStatsReceived_t
_2 -> m_CallbackUserStatsReceived
SELECTED -> 4
... -> 3

所以上述宏被替换为

1
_STEAM_CALLBACK_4

加上后面的部分,完整的展开为

1
_STEAM_CALLBACK_4 (, CStatsAndAchievements, OnUserStatsReceived, UserStatsReceived_t, m_CallbackUserStatsReceived);

可以看出 _STEAM_CALLBACK_HELPER 的参数都被丢弃掉了,只是为了替换成 _STEAM_CALLBACK_4 这个宏。这样做是出于什么考虑呢,后面会解释。

继续展开

_STEAM_CALLBACK_4

根据其定义

1
2
#define _STEAM_CALLBACK_4( _, thisclass, func, param, var ) \
CCallback< thisclass, param > var; void func( param *pParam )

其中的参数

1
2
3
4
5
_  ->  // 空
thisclass -> CStatsAndAchievements
func -> OnUserStatsReceived
param -> UserStatsReceived_t
var -> m_CallbackUserStatsReceived

最终宏展开为

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
2
void func(int x);
void func(int x, int y);

但是如果定义了两个同名的宏,前面的定义就会被后面的定义所覆盖。比如有两个宏定义:

1
2
#define MY_MACRO(X, Y) ...
#define MY_MACRO(X, Y, Z) ...

最终只会用到下面的宏定义,接受 3 个参数。

解决方案是利用了可变参数宏,比如定义两个宏:

1
2
#define MY_MACRO_2(X, Y) ...
#define MY_MACRO_3(X, Y, Z) ...

再定义一个帮助宏:

1
2
#define MY_MACRO_HELPER(_1, _2, _3, NAME, ...) NAME
#define MY_MACRO(...) MY_MACRO_HELPER(__VA_ARGS__, MY_MACRO_3, MY_MACRO_2)(_VA_ARGS__)

这样使用 MY_MACRO 宏时如果是两个参数,依次展开如下:

1
2
3
4
5
6
7
8
9
10
11
12
MY_MACRO(a, b)
// __VA_ARGS__ 替换为 a, b
// 得到
MY_MACRO_HELPER(a, b, MY_MACRO_3, MY_MACRO_2)(a, b)
// MY_MACRO_HELPER 中的
// _1 -> a
// _2 -> b
// _3 -> MY_MACRO_3
// NAME -> MY_MACRO_2
// 所以 MY_MACRO_HELPER(a, b, MY_MACRO_3, MY_MACRO_2) 替换为 MY_MACRO_2
// 得到
MY_MACRO_2(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。会根据参数数目的不同来使用不同的宏。前面的参数是为了占位用,所以后面会被丢弃。

参考链接