回调在C/C++中的几种实现方法(附代码)

回调在C/C++中的几种实现方法(附代码)

1.前言

回调(callback)是实现代码库功能扩展的一种重要机制。

代码库的开发者预留了几个功能扩展点,并以回调接口的形式进行规定。代码库的使用者实现回调接口,嵌入自己的扩展逻辑。程序运行时,用户的程序调用代码库,代码库在合适的时候再反过来回调应用逻辑,两者配合实现完整的功能。

Linux操作系统中的信号响应函数,pthread_create中的线程函数都是回调机制的一种应用。

可以看出,用户的逻辑与代码库其实是运行在同一个进程中的,因此相比消息队列等功能扩展手段,回调机制就有两个突出的优点:

  • 性能更好:代码库与应用逻辑之间通过函数调用共享数据,数据复制开销很小
  • 方便控制:代码库与应用逻辑之间是同步调用的,而不是异步的,方便控制运行时序

在C/C++中,有多种方面可以实现回调,下面一一说明。

2.C语言中的回调

来看代码(call_back_test_1.c):

//compile with : gcc -o call_back_test_1 call_back_test_1.c 

#include <stdio.h>
#include <unistd.h>

//1.定义回调接口
typedef void (*CALLBACK_FUNC_NAME)(char* s_arg, int i_arg, double d_arg);

//2.定义回调函数,跟回调接口的函数签名一致
void test_callback_func(char* s_arg, int i_arg, double d_arg)
{
    printf("print from callback, s_arg = %s, i_arg = %d, d_arg = %lf\n",
        s_arg, i_arg, d_arg);
}

//3.定义业务逻辑处理函数,传入回调接口
int event_loop(CALLBACK_FUNC_NAME callback_func_ptr)
{
    while(1)
    {
        callback_func_ptr("STRING", 1, 0.1);
        sleep(1);
    }
    return 0;
}

int main()
{
    //运行事件循环
    return event_loop(test_callback_func);
}

程序逻辑很简单,看注释就能看明白,不再解释。

程序运行结果如下:

3.C++中的回调(C99标准,成员函数)

这里有一个例子:

//compile with : g++ -o call_back_test_2 call_back_test_2.cpp 

#include <stdio.h>
#include <unistd.h>

//1.定义回调接口
typedef void (*CALLBACK_FUNC_NAME)(const char* s_arg, int i_arg, double d_arg);

//2.定义一个类,内部实现一个成员函数,当做回调函数用
class callback_t
{
public:
    callback_t()
    {
        //m_idx = 0;
    }
public:
    static void test_callback_func(const char* s_arg, int i_arg, double d_arg)
    {
        printf("[%ld]:print from callback, s_arg = %s, i_arg = %d, d_arg = %lf\n",
            m_idx, s_arg, i_arg, d_arg);
        
        ++m_idx;
    }
private:
    static long m_idx;
};

long callback_t::m_idx = 0;

//3.定义业务逻辑处理函数,传入回调接口
int event_loop(CALLBACK_FUNC_NAME callback_func_ptr)
{
    while(1)
    {
        callback_func_ptr("STRING", 1, 0.1);
        sleep(1);
    }
    return 0;
}

int main()
{
    //实例化一个类对象
    callback_t cb_obj;
    //传入这个对象的成员函数
    return event_loop(cb_obj.test_callback_func);
}

这里有几点需要注意:

1.类的成员函数必须得是static的,不然编译不过

因为从C语言继承来的这种方法,要求回调函数必须位于固定的位置,因此普通的类成员函数是无法满足要求的,只有静态成员函数才行。

而规定必须使用静态的成员函数,其实基本上等同于将C++当C用,面向对象的诸多好处都无法发挥,因此除非万不得已,不要使用这种回调机制。

2.传入回调函数是无需创建对象

使用类的静态函数作为回调函数时无需创建对象,直接用类名做限定符取静态成员函数即可。

int main()
{
    //传入这个静态成员函数
    return event_loop(callback_t::test_callback_func);
}

4.C++中的回调(C99标准,回调类)

直接看例子:

//compile with : g++ -o call_back_test_3 call_back_test_3.cpp 

#include <stdio.h>
#include <unistd.h>

//1.定义回调接口类
class callback_if_t
{
public:
    void virtual callback_func(const char* s_arg, int i_arg, double d_arg) = 0;
};

//2.实现回调接口类中规定的接口
class callback_impl_t: public callback_if_t
{
public:
    void virtual callback_func(const char* s_arg, int i_arg, double d_arg)
    {
        printf("print from callback, s_arg = %s, i_arg = %d, d_arg = %lf\n",
            s_arg, i_arg, d_arg);
    }
};

//3.定义业务逻辑处理函数,传入回调接口类
int event_loop(callback_if_t *p_callback_if)
{
    while(1)
    {
        p_callback_if->callback_func("STRING", 1, 0.1);
        sleep(1);
    }
    return 0;
}

int main()
{
    //实例化接口类实现对象
    callback_impl_t callback_imp;
    //运行事件循环
    return event_loop(&callback_imp);
}

这个例子就好很多,在实际生产中应用也比较广泛。

相比上个例子中直接把C++当C用,这里至少用到了泛型,值得安慰下。

这个例子中,回调的接口用一个类包装起来,接口全部定义为纯虚函数。业务逻辑类从回调接口类继承,实现其纯虚函数,嵌入业务逻辑。库进行回调的时候,C++的泛型机制会保证其调用到正确的实现。

这种方法虽然应用广泛,但也不是没有缺点。试想如果你想增加一个接口,这个类的符号就变了,新的接口类搭配老的实现类必然无法正常工作。

这种接口其实是把各个小的函数(泥团)打包成了大泥团,相比UNIX/Linux式的纯C系统调用接口,有种反其道而行之的感觉,不够简洁优雅。

5.C++中的回调(C++11标准,回调类)

到了C++11,终于看到了曙光,因为这一版C++引入了function、bind、lambda三个大杀器。

废话不多说,直接看代码。

//compile with : g++ -o call_back_test_4 call_back_test_4.cpp --std=c++11 

#include <functional>

#include <stdio.h>
#include <unistd.h>

// 1. 定义回调函数类型(std::function)
typedef std::function<void(const char*, int, double)> call_back_func_t;

// 2.1 普通的C-style的回调函数
void c_callback_func(const char* s_arg, int i_arg, double d_arg)
{
    printf("print from c_callback_func, s_arg = %s, i_arg = %d, d_arg = %lf\n",
        s_arg, i_arg, d_arg);
}

// 2.2 C++类成员变量回调函数
class cpp_callback_t
{
public:
    void cpp_callback_func(const char* s_arg, int i_arg, double d_arg)
    {
        printf("print from cpp_callback_func, s_arg = %s, i_arg = %d, d_arg = %lf\n",
            s_arg, i_arg, d_arg);
    }
};

// 3. 事件循环(可接受参数为std::function)
int event_loop(call_back_func_t cb_func)
{
    int loop_times = 5;
    while(loop_times--)
    {
        if(cb_func)
            cb_func("STRING", 1, 0.1);
        sleep(1);
    }
    return 0;
}

int main()
{
    //std::fcuntion类型的变量
    call_back_func_t cb_func;
    
    // 1. 可接受普通C-style回调函数
    cb_func = c_callback_func;
    event_loop(cb_func);
    printf("------------------\n");

    // 2.1 可通过std::bind绑定类的普通成员函数(绑定方式1)
    cpp_callback_t cpp_cb;
    cb_func = std::bind(&cpp_callback_t::cpp_callback_func, cpp_cb, 
        std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
    event_loop(cb_func);
    printf("------------------\n");

    // 2.2 可通过std::bind绑定类的普通成员函数(绑定方式2)
    typedef std::function<void(cpp_callback_t, const char*, int, double)> call_back_func_t_2;
    call_back_func_t_2 cb_func_2 = &cpp_callback_t::cpp_callback_func;
    cb_func_2(cpp_cb, "STRING_2", 1, 0.1);
    printf("------------------\n");

    // 3. 还能绑定一个lambda函数!!!
    auto lambda_cb_func = [](const char* s_arg, int i_arg, double d_arg)->void
        {
            printf("print from lambda_callback_func, s_arg = %s, i_arg = %d, d_arg = %lf\n",
                s_arg, i_arg, d_arg);
        };
    cb_func = lambda_cb_func;
    event_loop(cb_func);

    return 0;
}

在这个版本中,我们使用std::function声明回调函数类型,光是这个参数列表就简洁不少:

然后我们以各种格式实现了回调函数,包括:

普通的C语言风格的函数:

这种函数无需特殊绑定,能直接复制给std::function类型的变量。

C++(非静态)成员函数风格的函数:

这种函数需要通过std:bind接口才能赋值给std::function类型的变量。bind的方式有两种,一种是bind时指定类对象,调用时不指定;一种是bind时不指定类对象,调用时指定:

更惊喜的是,他还能绑定lambda函数!

运行结果如下:

6.总结

如果你立志写现代C++程序,那么强烈建议你选择C++11风格的实现方式;如果你的程序需要保持较好的兼容性,或者只能用C语言,那么你只能选择C语言风格的实现方式了。另外两种,一个不伦不类,一个难以升级,能不用就不用!

发表回复