在C++中使用回调函数的几种方式

时间:2021-11-14 来源:未知网络 作者:996建站网

回调函数(callback)在很多地方是非常关键的,尤其是需要事件和结果解耦的时候。这里结合一些现实中的例子,看看怎么在C++中使用回调函数。重点看如何绑定类的成员函数作为回调。

主要方式

C++的特性是非常庞大的,其中实现回调的方式各种各样,再加上传值传引用什么的,非常复杂。这里只看常见的几种方式。

函数指针

用一个简单的函数指针来实现回调。可以直接取函数的地址,作为指针传来传去。这个指针只能携带一个东西,就是函数的地址,所以难以传递其他数据。

C里面的qsort就用了这个方式,由用户来指定一个元素比较函数,实现对任意数据类型排序的功能。

其实非要传参数好像也可以,下下小节有个StackOverflow的问题链接里有描述,可以使用”Thunk“。个人理解就是,动态生成的函数,这个函数里可以有数据。类似C++里的capturing lambda,或者operator()在纯C里的实现。

函数指针+用户指针

用一个函数指针,加一个用户指针来实现。用户指针通常是一个void *类型。这个应该是C里面最常用的了,非常强大,应该可以实现任意功能。不过因为有个void *,需要一些类型转换才能正常用。void *还可以换成intptr_t之类的类型,至少需要可以容纳一个指针。

Win32 API里的EnumWindows就是这种,用户提供一个函数指针和一个用户数据,这个函数指针会接收系统里所有的窗口句柄HWND和调用EnumWindows时传入的这个用户数据。

函数指针+在主对象里的用户指针

用一个函数指针,加一个隐藏在主对象里的用户指针。这里的“主对象”指注册回调的目标对象,或者说将要调用这个回调的对象。虽然看上去隐藏的有点深,但实际上非常有用。这个回调的第一个参数需要是主对象,然后提供了接口从这个主对象里设置和取出用户指针。

Win32的消息处理函数,也就是处理窗口Message的函数lpfnWndProc,就可以用这种方式。SetWindowLongPtr(hwnd, GWLP_USERDATA, ptr)可以给窗口添加用户指针。WindowProc的第一个参数是窗口句柄HWND,GetWindowLongPtr(hwnd, GWLP_USERDATA)可以从这个HWND里获取添加的用户指针。也可以使用cbWndExtra分配更多的内存来保存user_data(Scintilla就使用了这个方式)。GLFW的消息处理也非常类似,有glfwSetWindowUserPointer和glfwGetWindowUserPointer来对GLFWwindow设置和获取用户指针,而各种回调的第一个参数就是GLFWwindow*。

就算主对象里没有提供设置user_data的接口,也可以很简单绕过去。弄一个全局的map,记录主对象和用户指针的对应关系。这样通过访问这个全局map对象,就可以实现类似SetWindowUserData和GetWindowUserData的功能。脚本binding也可以用类似的方法做脚本对象和C++对象的对应。

下面有个链接,里面详细描述了Win32编程里各种把WndProc和this 对应起来的方法,值得一看:

stackoverflow.com/quest

std::function

是C++11新加的东西,只要是可调用的东西都可以包装,简单万能,类型安全。

cocos2d-x里的addTouchEventListener等各种回调全部用的std::function,非常灵活。

类对象

直接给定一个带作为回调的虚函数作为基类,用户自行派生,覆盖掉作为回调的虚函数。也就是用派生类的对象代替函数指针。这种方式还算简单易懂,并且也是类型安全的。应该也可以覆盖operator()。

Box2D和PhysX物理引擎都是用的这种方式,很巧。例如Box2D的b2ContactListener,接收碰撞消息。例如PhysX的raycast、sweep、overlap等,都是基于PxHitCallback基类来实现的。有个好处是可以同时封装好几个相关的虚函数。

模板函数对象

模板非常强大,如果需要接收任何可调用的东西,参数原封不动处理,最小的overhead,模板应该是终极选择。

标准库里大量使用了模板函数对象。例如std::sort,接收用户指定的比较两个元素的“函数”。例如std::for_each,接收用户指定的处理元素的“函数”。这里的函数可以是任何可调用的东西。

例子

这里看几个例子,包含了一些常见的用法和常见的问题。

函数指针

如果只用一个函数指针来实现回调,会出现一个难以绕过的问题,就是无法绑定捕获变量的lambda,无法绑定类普通成员函数。

#include <iostream>

// callback function pointer type
typedef void (*process_func)(int element);

// call the callback
void call_process(process_func func) {
    func(42);
}

void process1(int element) {
    std::cout << "process1: " << element << "\n";
}

struct Process3 {
    static void process3(int element) {
        std::cout << "process3: " << element << "\n";
    }
};

struct Process5 {
    void process5(int element) {
        std::cout << "process5: " << element << "\n";
    }
};

int main() {
    call_process(&process1);
    // OK

    call_process([](int element) {
        std::cout << "process2: " << element * 10 << "\n";
    });
    // OK: non capturing lambda converted to plain function pointer

    call_process(&Process3::process3);
    // OK: class static function

    int n = 10;
    call_process([n](int element) {
        std::cout << "process4: " << element * n << "\n";
    });
    // ERROR: capturing lambda cannot convert to plain function pointer
    // cannot convert argument 1 from 'main::<lambda_96300c2087f81597c409144da35404b7>' to 'process_func'

    Process5 process5_obj;
    call_process(&process5_obj.process5);
    // ERROR: cannot use bound member function like this
    // error C2276: '&': illegal operation on bound member function expression

    return 0;
}

在C++中使用回调函数的几种方式插图

微信扫一扫 关注公众号

微信扫一扫 使用小程序

百度扫一扫 使用小程序