笔者在入门cocos2d-x游戏开发时,希望实现一个功能:点击菜单项、转到下一个页面。这里用到的语句是:
bool HelloWorld::init()
{
......
MenuItemFont* item1 = MenuItemFont::create("Start",
CC_CALLBACK_1(HelloWorld::menuItem1Callback, this));
......
}
void HelloWorld::menuItem1Callback(Ref* pSender)
{
MenuItem* item = (MenuItem*)pSender;
log("Touch Start Menu Item %p", item);
}
create函数的功效是,点击菜单项,就调用参数里的“menuItem1Callback”函数。
很好奇,CC_CALLBACK_N是什么?它是如何实现回调函数的?为什么回调函数的形参是Ref*,为什么还传进去一个this指针?
一、回调函数的作用
回调函数本身的定义与普通函数无异,关键在于“回调”二字,就是说,它将被别的函数选择调用,别的函数不调用,它就不工作,反之就工作。这里,回调函数以函数指针的方式,传递到别的函数里作为参数,而不是被别的函数以代码块形式调用。
那么为什么需要回调函数以函数指针的形式传参进“主调函数”呢?因为我们希望赋予主调函数一种权限,就是它可以根据需要选择回调函数,这种选择就是通过回调函数指针的选取实现的。
举个例子,如下。
#include <iostream>
void programA_FunA1() { printf("I'am ProgramA_FunA1 and be called..\n"); }
void programA_FunA2() { printf("I'am ProgramA_FunA2 and be called..\n"); }
void programB_FunB1(void (*callback)()) {
printf("I'am programB_FunB1 and be called..\n");
callback();
}
int main(int argc, char **argv) {
programA_FunA1();
programB_FunB1(programA_FunA2);
}
二、std::function处理回调函数
std :: function是一个通用的多态函数包装器。 std :: function的实例可以存储,复制和调用任何可调用的目标 :包括函数,lambda表达式,绑定表达式或其他函数对象,以及指向成员函数和指向数据成员的指针。
它的好处就在于,可以将回调函数妥为包装,作为参数传到别的函数里去,实现回调的过程。特别是和std::bind配合使用,更彰显其威力。
例如:function<int(int,int)> func;
则 function类的实例func可以指向 返回值为int型,有两个形参都为int型的函数。
注意,普通函数的函数名可以隐式转化为函数指针。
#include <functional>
#include <iostream>
int f(int a, int b)
{
return a+b;
}
int main()
{
std::function<int(int, int)>func = f;
cout<<f(1, 2)<<endl; // 3
system("pause");
return 0;
}
当function处理类的非静态成员函数时:
#include <iostream>
#include <functional>
using namespace std;
class Plus
{
public:
int plus(int a, int b)
{
return a + b;
}
};
int main()
{
Plus p;
function<int(Plus*,int, int)> f = &Plus::plus;
//function<int(const Plus,int, int)> f = &Plus::plus;
cout << f(p,1, 2) << endl; //3
system("pause");
return 0;
}
大家注意,Plus类里面的plus函数明明只有两个int参数,为什么在main函数里多了一个Plus&?为什么等号后面也多了一个&?
第一个问题的原因是,类的成员函数都会被隐式的传递一个this指针,因此Plus::plus的真实类型是:int(Plus*,int, int)。Plus*的作用是告诉编译器,这个非静态成员函数将使用哪个实例化对象来调用。如果想调用的是静态成员函数,就不存在这个问题,因为静态成员函数可以直接靠类调用。
第二个问题的原因是,编译器不会将对象的成员函数隐式转换成函数指针,因此必须在Plus::plus前加&。
三、bind绑定函数
std::bind的头文件是 <functional>,它是一个函数适配器,接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。
std::bind的返回值是可调用实体,可以直接赋给std::function。
我们尝试将bind绑定类的非静态成员函数:
class Base { void display_sum(int a1, int a2) { std::cout << a1 + a2 << '\n'; } int m_data = 30; }; int main() { Base base; auto newiFunc = std::bind(&Base::display_sum, &base, 100, std::placeholders::_1); f(20); // should out put 120. }
注意,bind的参数先后为:要调用的非静态类成员函数指针(必须加&)、调用者地址(&)、绑定指定的参数或者占位符。这样一来,我们下次调用该函数时,只需人为指定一个参数,另一个参数由程序员已经制定好了。
bind可以返回一个std::function<void(int)>
这样有什么好处呢?不是有缺省参数吗?为什么要多此一举?
试想,假如有一个std::function类型 : std::function<void(int)>,原本这个回调函数display_sum是有两个参数,不可能成为该function类型的,现在我们人为砍掉他一个参数,就可以用bind将其转变为该function类型,实现调用。
四、回到 CC_CALLBACK_N
来看其定义:
#define CC_CALLBACK_0(__selector__,__target__, ...) std::bind(&__selector__,__target__, ##__VA_ARGS__) #define CC_CALLBACK_1(__selector__,__target__, ...) std::bind(&__selector__,__target__, std::placeholders::_1, ##__VA_ARGS__) #define CC_CALLBACK_2(__selector__,__target__, ...) std::bind(&__selector__,__target__, std::placeholders::_1, std::placeholders::_2, ##__VA_ARGS__) #define CC_CALLBACK_3(__selector__,__target__, ...) std::bind(&__selector__,__target__, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, ##__VA_ARGS__)
不要被吓到了。我们刚刚提到,bind的参数分别为:要调用的非静态类成员函数指针(必须加&)、调用者地址(&)、绑定指定的参数或者占位符。现在CC_CALLBACK_1简化了这一过程,只需要传入不带&的成员函数名字和调用者指针即可。
也就是说,CC_CALLBACK_N这一系列宏定义,都是bind的变体。只不过现在,我们不需要在成员函数前面加&,也不需要人工指定占位符了。
bool HelloWorld::init()
{
......
MenuItemFont* item1 = MenuItemFont::create("Start",
CC_CALLBACK_1(HelloWorld::menuItem1Callback, this));
......
}
void HelloWorld::menuItem1Callback(Ref* pSender)
{
MenuItem* item = (MenuItem*)pSender;
log("Touch Start Menu Item %p", item);
}
回到刚刚的代码。查源代码得:
MenuItemAtlasFont * MenuItemAtlasFont::create
(const std::string& value, const std::string& charMapFile, int itemWidth, int itemHeight, char startCharMap,
const ccMenuCallback& callback)
typedef std::function<void(Ref*)> ccMenuCallback;
也就是说,create接受一个带有void(Ref*)类型的std::function。
这样一来,CC_CALLBACK_N中只有N=1适合。
于是我们传入回调函数名menuItem1Callback,又传入helloworld的指针this。
回调函数剩余的那个没被指定的参数是谁呢?查源代码可得,是该菜单项类型MenuItemAtlasFont的父类的父类MenuItem的一个指针,这个由系统通过其他方式调用,我们不用管。
最后一个问题,为什么typedef std::function<void(Ref*)> ccMenuCallback 要接受的是带有Ref*类型的回调函数呢?因为Ref是cocos的鼻祖类,传入它,是完全之策,不论你真正想使用哪一个类型,只需将Ref向下转换到实际的类型,或者使用多态调用即可。
完毕。同济大学 wst