再见了!函数指针!

"c++11 std::bind 和 std::function介绍"

Posted by Simon on May 27, 2020

“Better code, better life. ”

再见了函数指针!

在C语言中,如果想实现回调函数之类功能,函数指针大概是不能避免的。但是,凡是涉及到指针的东西,都很危险!由于C语言中,指针只是一个地址,两个任何类型的指针可以通过void*来互相转换,并且可以通过编译!!所以在大型程序中,经常出现一个指针被多次cast后得到了一个非预期类型,导致程序崩溃。到了C++,新标准增加了std::bindstd::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函数的声明后,很容易就知道如何调用。