Use examples of c + + callback function and std::function

Understanding callbacks

In actual projects, there is often a need to use callback functions, such as:

  • In the communication between the two parties, the data of the other party is waiting to be received and processed, such as TCP communication using socket
  • Timer event. When the timer ends, a task needs to be processed
  • The trigger of the signal needs to perform a task

In synchronous programming, you need to start a special thread to block, listen and handle those events that may occur in the future. In asynchronous programming, you only need to use callback functions.

In this scenario, it can be simply understood as: I register a processing function for an event, and the function will be called automatically when the event occurs. This function can be understood as a callback function.

In fact, the callback function is provided, that is, the pointer (address) of the processing function is provided. After an event occurs, the function pointer is called to call the function.

Simple process:

  • Define a normal function as the processing function
  • Register the address of the handler function with the caller
  • The caller calls the processing function through the function pointer at the appropriate time

method

  1. c-style function pointer

    This is achieved by passing the address of the callback function to the called function, as follows:

    void handle(char *s) // Processing function
    {
    	printf("%s\n", s);
    }
    
    void (*pfoo) (char *); //The function pointer takes char * as the parameter and returns void, which is consistent with the signature of the processing function
    
    // Simplify variable definition of function pointer type
    typedef void(*pfoo)(char *);
    // Later, you can pass a function of type pfoo to a trigger that requires a pfoo parameter
    void event_trigger(pfoo p)
    {
    	p("event happened!");
    }
    
    int main(void)
    {
    	event_trigger(handle);
    	
    	return 0;
    }
    
    
  2. std::function

    General polymorphic function wrapper, which can store, copy and trigger any callable object, including function, Lambda expression, bind expression, function pointer and other function objects.

    For more information on Lambda expressions, refer to Lambda expressions in c + +.

    Examples are as follows (from cppreference.com):

    #include <functional>
    #include <iostream>
     
    struct Foo {
        Foo(int num) : num_(num) {}
        void print_add(int i) const { std::cout << num_+i << '\n'; }
        int num_;
    };
     
    void print_num(int i)
    {
        std::cout << i << '\n';
    }
     
    struct PrintNum {
        void operator()(int i) const
        {
            std::cout << i << '\n';
        }
    };
     
    int main()
    {
        // store a free function
        std::function<void(int)> f_display = print_num;
        f_display(-9);
     
        // store a lambda
        std::function<void()> f_display_42 = []() { print_num(42); };
        f_display_42();
     
        // store the result of a call to std::bind
        std::function<void()> f_display_31337 = std::bind(print_num, 31337);
        f_display_31337();
     
        // store a call to a member function
        std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
        const Foo foo(314159);
        f_add_display(foo, 1);
        f_add_display(314159, 1);
     
        // store a call to a data member accessor
        std::function<int(Foo const&)> f_num = &Foo::num_;
        std::cout << "num_: " << f_num(foo) << '\n';
     
        // store a call to a member function and object
        using std::placeholders::_1;
        std::function<void(int)> f_add_display2 = std::bind( &Foo::print_add, foo, _1 );
        f_add_display2(2);
     
        // store a call to a member function and object ptr
        std::function<void(int)> f_add_display3 = std::bind( &Foo::print_add, &foo, _1 );
        f_add_display3(3);
     
        // store a call to a function object
        std::function<void(int)> f_display_obj = PrintNum();
        f_display_obj(18);
    }
    
    

    The output is as follows:

    -9
    42
    31337
    314160
    314160
    num_: 314159
    314161
    314162
    18
    
    

In short, std::function is powerful and easy to use. You can experience its beauty in use.

Cross calls between multiple classes

Often in such scenarios, class a needs an instance of class B, and class B needs to call the interface of class A. consider the following scenarios:

A data processing platform (implemented by DataMgr class) needs to process the data sent by customers. TCP data sending and receiving is implemented by TcpMgr class, but the data processing is completed in DataMgr. The process is as follows:

  1. DataMgr sends and receives data through TcpMgr
  2. After TcpMgr receives the data, it processes the data through the functions in DataMgr

The following code snippet (omit the rest):

// DataMgr.h file

#include "TcpMgr.h"

class DataMgr 
{
public:
	void Init();
	void HandleData(const string &data)
	{
		// ...
	}
	
private:
	boost::shared_ptr<TcpMgr> m_pTcpMgr;
};

// TcpMgr.h file
class TcpMgr
{
public:
	void OnRecvedData(const string &data)
	{
		// Call DataMgr::HandleData
		// How to call?
	}
}

There are many methods to achieve the purpose of the above call, such as saving the instance of DataMgr in TcpMgr (pre declaration of classes is required during cross reference). Here is how to use callback functions.

The idea is: define an std::function object in TcpMgr, then provide an interface for external calls to set the object, and then use the object to process data. The specific methods are as follows:

  • Define std::function objects in TcpMgr, such as: std::function < void (const string &) >
    m_handleDataCb;
  • An interface is provided for external calls to set the object, such as void setdatahandlecb (STD:: function < void (const)
    string &)> cb) { m_handleDataCb = cb; }
  • Use SetDataHandleCb to set the callback function in DataMgr
  • Use this object to process data: m_handleDataCb(msg);

The example code is as follows:

// DataMgr.h file

#include "TcpMgr.h"

class DataMgr 
{
public:
	void Init()
	{
		m_pTcpMgr = std::make_shared<TcpMgr>();
		m_pTcpMgr->SetDataHandleCb(std::bind(&DataMgr::HandleData, this, _1)); // Set callback function for TcpMgr in initialization
	}
	void HandleData(const string &data)
	{
		// ...
	}
	
private:
	boost::shared_ptr<TcpMgr> m_pTcpMgr;
};

// TcpMgr.h file
class TcpMgr
{
public:
	void SetDataHandleCb(std::function<void(const string &)> cb)
	{
		m_handleDataCb = cb; 
	}
	void OnRecvedData(const string &data)
	{
		// Call DataMgr::HandleData
		m_handleDataCb(data);
	}
private:
	std::function<void(const string &)> m_handleDataCb;
}

As mentioned above, some problems caused by cross reference can be avoided and the data processing relationship between classes can be better realized.

Summary
Callback functions are widely used in practical projects. C 11 provides convenient ways such as Lambda expressions. Local use in practical programming can significantly improve the readability and maintainability of the program.

reference material
std::function
Lambda expression in c + +

Tags: C C++

Posted on Tue, 30 Nov 2021 23:45:48 -0500 by OriginalSixRules