“Better code, better life. ”
再见了函数指针!
在C语言中,如果想实现回调函数之类功能,函数指针大概是不能避免的。但是,凡是涉及到指针的东西,都很危险!由于C语言中,指针只是一个地址,两个任何类型的指针可以通过void*来互相转换,并且可以通过编译!!所以在大型程序中,经常出现一个指针被多次cast后得到了一个非预期类型,导致程序崩溃。到了C++,新标准增加了std::bind
和std::function
两个模板方法,可以做到对回调函数/函数指针的完全替代。
std::bind
bind
是一个模板方法,其原型为
template<typename _Func, typename... _Args>
inline typename
_Bind_helper<Type T, _Func, _Args...>::type
bind(_Func&& __f, _BoundArgs&&... __args){
typedef _Bind_helper<false, _Func, _BoundArgs...> __helper_type;
return typename __helper_type::type(std::forward<_Func>(__f),
std::forward<_BoundArgs>(__args)...);
}
template<bool _SocketLike, typename _Func, typename... _BoundArgs>
struct _Bind_helper
: _Bind_check_arity<typename decay<_Func>::type, _BoundArgs...>
{
typedef typename decay<_Func>::type __func_type;
typedef _Bind<__func_type(typename decay<_BoundArgs>::type...)> type;
};
template<typename _Tp>
class decay
{
typedef typename remove_reference<_Tp>::type __remove_type;
public:
typedef typename __decay_selector<__remove_type>::__type type;
};
这里直接粘贴了部分化简后的关键源码,其中一些函数没有很细致的看,但是只看这个声明,我们可以发现:
- bind第一个模板参数是一个
callable
的对象,它可以是函数或者重载了()
操作符的类;第二个是一个可变模板参数,它应该是第一个callable
对象的参数。 - bind返回一个
_BIND
类型,即_Bind_helper<Type T, _Func, _Args...>::type
。 - bind的两个形参都具有
&&
万能引用型别。 - bind在底层其实对
_Func
和_Args
做了remove_reference
处理的,这里其实有坑,后面会讲。
std::placeholder
通过bind,我们可以获得一个callable
的对象,通过()
可以执行并获取结果,举个例子:
#include <iostream>
#include <functional>
using namespace std;
void add(int x, int y) {
cout << x + y << endl;
}
int main(){
auto f = bind(add, 4, 5);
f();
}
output:
9
在这里bind的作用其实和lambda表达式类似,它创造了一个可以在未来执行的并获取结果的函数。上面的例子中,在bind的时候就已经确定了传递的参数,但实际上,借助std::placeholder
我们可以实现调用时传参。同样的例子,稍作修改:
int main(){
auto f = bind(add, 4, std::placeholders::_1);
f(5);
}
output:
9
std::placeholders::_1
这是一个占位符,表示第一个参数,同理可以有std::placeholders::_2
等等。
std::function
如上所述,bind
提供了一种方法可以让我们创造一个在将来调用的方法,但是,我们如何将这个方法当作参数传递呢?这时本篇要讲的另一个模板类std::function
就排上用场了。我们可以将bind的结果复制给一个function对象。如下代码所示:
#include <iostream>
#include <functional>
void add(int x, int y) {
std::cout << x + y << std::endl;
}
int main() {
std::function<void(int)> foo = bind(add, 4, std::placeholders::_1);
foo(3);
std::function<void(int, int)> foo1 = bind(add, std::placeholders::_1, std::placeholders::_2);
foo1(3, 4);
}
output:
7
7
相对于C语言的void*
传递函数指针来说,function
最大的优点是提供了类型检查。function
作为一个模板类,会在编译期展开成对应的代码,并做类型检查,暴露代码问题,而不用等到运行期cast
后发现类型错误导致程序崩溃。
注意:前面讲bind的时候提到过,这里在传参数的时候,对于传进的参数是做了remove_reference
处理的,这主要是因为,考虑到bind的结果是在将来起作用,而调用的时刻又很难保证传入的参数是否有效,所以这里bind采用了值传递的方式,即便你的函数声明是引用传参也一样。如果你想在函数体内修改这个方法,就需要借助std::ref来构造一个reference_wapper。
用处
我用到这两个特性是因为最近要用C++写一个Mysql的查询功能,目前已有一些表需要做查询,我发现每次做查询的时候都要在Mysql类里新增一个方法,如queryTable1(),queryTable2(),这两个方法可以只有处理查询结果的部分不一样,于是很自然就想到,为什么不用回调函数做抽象呢?于是就有了类似下面的代码:
class MysqlClient{
public:
template<typename F>
void query(const char* sql, F &&f){
MYSQL_RES *res;
int flag = mysql_real_query(this->_conn, sql, (unsigned long) strlen(sql));
if (flag) {
perror("Query error:%d, %s\n", mysql_errno(this->_conn), mysql_error(this->_conn));
return;
}
res = mysql_store_result(this->_conn);
f(res);
}
};
这样做的好处就是,MysqlClient类只需要提供一个query方法,每当有其他模块需要查询表结果时,只要把sql和相应的回调传入即可。
这里我试图给query加了一个模板参数当作回调函数,运行结果也符合预期,但是总让我感觉这代码有点点硬了,后续如果有人看到这个函数,可能要找到调用它的地方才知道这里是怎么回事,所以我尝试把代码改进成这样:
#include <iostream>
#include <string>
#include <vector>
#include <functional>
class MysqlClient{
public:
static void query(const char* sql, std::function<void(MYSQL_RES*)> &f){
MYSQL_RES *res;
int flag = mysql_real_query(this->_conn, sql, (unsigned long) strlen(sql));
if (flag) {
perror("Query error:%d, %s\n", mysql_errno(this->_conn), mysql_error(this->_conn));
return;
}
res = mysql_store_result(this->_conn);
f(res);
}
};
int queryTable() {
const char * sql = "select * from table";
std::vector<std::string> result;
auto f = [](std::vector<std::string>&, MYSQL_RES*){
//do something
};
std::function<void(MYSQL_RES*)> callback = std::bind(f, std::ref(result), std::placeholders::_1);
MysqlClient::query(sql, callback);
return 0;
}
这样,起码别人看到query函数的声明后,很容易就知道如何调用。