自学内容网 自学内容网

C++--包装器std::function和绑定std::bind

std::function

std::function是一个类型安全的通用函数包装器。它可以存储、复制和调用任何可以调用的目标(如普通函数、Lambda表达式、函数指针、成员函数指针等),std::function 对象可以通过 operator() 来调用所存储的可调用对象。这使得std::function在需要使用回调函数或将函数作为参数传递时非常有用。

语法规则

声明

要使用 std::function,首先需要包含 <functional> 头文件。然后,可以声明一个 std::function 对象,指定其可调用对象的签名(即返回类型和参数列表)。

std::function<返回类型(参数列表)> functionName = 可调用对象;

例如,一个接受两个 int 参数并返回一个 int 值的可调用对象可以这样声明:

std::function<int(int, int)> func;

然后可以使用 std::function 的构造函数来初始化。

赋值

你可以将函数、lambda 表达式或其他可调用对象赋值给 std::function 对象。例如:

// 赋值一个lambda表达式
func = [](int x, int y) { return x + y; };

// 调用
int result = func(2, 3); // result 为 5

std::function 可以赋值给相同签名的另一个 std::function 对象。

void print(int val) 
{
    std::cout << val << std::endl;
}

// 声明一个 std::function 对象
std::function<void(int)> f = print;
std::function<void(int)> h;
h = f; // 合法赋值

std::function<void(int, double)> j;
j = h; // 编译错误,类型不匹配

调用
std::function 对象通过 operator() 来调用所存储的可调用对象。与普通函数调用的使用一样。

示例

#include <iostream>  
#include <functional>  
  
// 普通函数  
void print_hello() 
{  
    std::cout << "Hello, World!" << std::endl;  
}  
  
// Lambda 表达式  
auto print_world = []() 
{  
    std::cout << "Hello, World from Lambda!" << std::endl;  
};  
  
// 函数对象  
struct PrintMessage 
{  
    void operator()() const 
    {  
        std::cout << "Hello from function object!" << std::endl;  
    }  
};  
  
int main() 
{  
    // 存储普通函数  
    std::function<void()> func1 = print_hello;  
    func1();  
  
    // 存储 Lambda 表达式  
    std::function<void()> func2 = print_world;  
    func2();  
  
    // 存储函数对象  
    std::function<void()> func3 = PrintMessage();  
    func3();  
  
    return 0;  
}

使用场景

  1. 回调函数: 在图形用户界面程序或网络编程中,经常需要定义会调用函数。
  2. 事件处理: 在观察者模式中,可以用std::function存储和调用事件处理函数。
  3. 作为函数参数和返回值: 方便传递函数或存储函数以在其他地方调用。

应用一:回调函数

在图形用户界面(GUI)程序或网络编程中,回调函数是非常常见的。它们允许程序在特定事件发生时(如用户点击按钮或数据接收完毕)执行特定的代码。

GUI 回调函数示例(假设我们有一个简化的 GUI 框架):

#include <iostream>
#include <functional>

// 假设的按钮类
class Button 
{
public:
    void setOnClickListener(std::function<void()> listener) 
    {
        onClick = listener;
    }

    void click() 
    {
        if (onClick) 
        {
            onClick();
        }
    }

private:
    std::function<void()> onClick;
};

void onButtonClick() 
{
    std::cout << "Button was clicked!" << std::endl;
}

int main() 
{
    Button button;
    button.setOnClickListener(onButtonClick);
    button.click(); // 输出: Button was clicked!
    return 0;
}

在这个例子中,Button 类有一个 setOnClickListener 方法,它接受一个 std::function<void()> 类型的回调函数。当用户点击按钮时(在这里模拟为调用 click 方法),回调函数会被调用。

应用二:事件处理

在观察者模式中,std::function 可以用来存储和调用事件处理函数。当某个事件发生时,所有注册的事件处理函数都会被调用。

#include <iostream>
#include <vector>
#include <functional>

class EventSource 
{
public:
    void registerObserver(std::function<void()> observer) 
    {
        observers.push_back(observer);
    }

    void notifyObservers() 
    {
        for (auto& observer : observers) 
        {
            observer();
        }
    }

private:
    std::vector<std::function<void()>> observers;
};

void handleEvent1() 
{
    std::cout << "Event 1 handled." << std::endl;
}

void handleEvent2() 
{
    std::cout << "Event 2 handled." << std::endl;
}

int main() 
{
    EventSource eventSource;
    eventSource.registerObserver(handleEvent1);
    eventSource.registerObserver(handleEvent2);
    eventSource.notifyObservers(); // 输出: Event 1 handled. Event 2 handled.
    return 0;
}

在这个例子中,EventSource 类维护了一个 std::vector<std::function<void()>> 类型的观察者列表。当调用 notifyObservers 方法时,所有注册的观察者都会被调用。

应用三:作为函数参数和返回值

将函数作为参数传递或作为返回值返回可以增加代码的灵活性和可重用性。std::function 使得这种操作变得简单且类型安全。

#include <iostream>
#include <functional>

// 一个接受函数作为参数并调用它的函数
void processFunction(std::function<void()> func) 
{
    func();
}

// 一个返回函数的函数
std::function<void()> createGreetingFunction(const std::string& name) 
{
    return [name]() {
        std::cout << "Hello, " << name << "!" << std::endl;
    };
}

int main() 
{
    // 将函数作为参数传递
    processFunction([]() { std::cout << "Function passed as argument." << std::endl; });

    // 获取并调用返回的函数
    auto greetAlice = createGreetingFunction("Alice");
    greetAlice(); // 输出: Hello, Alice!

    return 0;
}

在这个例子中,processFunction 接受一个 std::function<void()> 类型的参数并调用它。createGreetingFunction 返回一个 std::function<void()> 类型的函数对象,该函数对象捕获了一个字符串参数并在调用时打印出来。

应用四:包装类成员函数

std::function 能包装类成员函数:静态成员函数和非静态成员函数

#include <iostream>
#include <functional>

class Puls
{
public:
static int plus_i(int a, int b)
{
return a + b;
}
double plus_d(double a, double b)
{
return a + b;
}
};

int main()
{
// 包装类静态成员函数
std::function<int(int, int)>func2 = Puls::plus_i;
std::cout << "plus_i = " << func2(1, 2) << std::endl;

// 包装类非静态成员函数

// 第一种写法:对象地址
Plus p;
std::function<double(Plus*, double, double)>func3 = &Plus::plus_d;
std::cout << "plus_d = " << func3(&p, 1.5, 2.3) << std::endl;
// 第二种写法:匿名对象
std::function<double(Plus, double, double)>func3 = &Plus::plus_d;
std::cout << "plus_d = " << func3(Plus(), 1.5, 2.3) << std::endl;
return 0;
}
  • 如果要包装类非静态成员函数,需要对象指针或者对象去调用。因为要取成员函数的地址,需要在前面加上一个&
  • 还要注意的是,非静态成员函数还有一个隐含的参数 this,在调用传递参数时也需要传入对象。

不过这两种写法,还是不够优雅~ 因为我们在调用某一个函数时,只想传递在参数列表中暴露出来的参数,而不传递隐含的参数 this

为了达到这种效果,就需要使用 std::bind 改变函数调用参数的个数(具体用法下面讲解):

int main()
{
// 包装类非静态成员函数

std::function<double(double, double)>func4 = std::bind(&Plus::plus_d, Plus(), \
std::placeholders::_1, std::placeholders::_2) ;
std::cout << "plus_d = " << func4(1.5, 2.3) << std::endl;

return 0;
}

实例:逆波兰表达式求值

这里有一个OJ题目,可以使用function更简单的解决:150. 逆波兰表达式求值

这里原先思路是:判断波兰表达式每一个字符,如果遇到操作数入栈,如果遇到操作符,出栈两个操作数,使用switch case语句匹配执行对应的运算,在将结果入栈:

if(e == "+" || e == "-" || e == "*" || e == "/")
{
    int rightNum = st.top();
    st.pop();
    int leftNum = st.top();
    st.pop();

    switch(e[0])
    {
        case '+' : st.push(leftNum+rightNum);break;
        case '-' : st.push(leftNum-rightNum);break;
        case '*' : st.push(leftNum*rightNum);break;
        case '/' : st.push(leftNum/rightNum);break;
    }
}
else
{
    // 操作数转化成整型后,入栈
    st.push(stoi(e));
}

这里可以使用std::map容器,将操作符和对应操作函数的映射关系保存下来,这样遇到一个操作符时,就执行对应的操作,不需要在进行switch case判断了:

并且这里使用封装器对执行函数进行封装,而不是直接使用函数对象中的任一一个,是因为每一个对象都有各自的缺点:

  • 函数指针:类型写起来比较复杂。
  • 仿函数:不同仿函数的类型不同,一个仿函数一个类型,不能存放在同一个容器进行管理使用(即使参数和返回值相同)
  • lambda表达式:语法层面没有类型

如果不使用std::function

要使用map容器管理加法、减法、乘法和除法执行的函数时,需要传递类型,而这里只有函数指针符合条件:

  • 如果使用仿函数,4个运算符对应4个函数,就要写4个仿函数,每一个仿函数的类型不一致,无法传递给map的模板参数。
  • lambda表达式没有类型,更无法进行传递。
  • 如果使用函数指针,就需要写出4个函数,并且其函数指针类型写起来也不方便(可以看下面代码感受一下)。
// 定义基本运算函数
int add(int x, int y) { return x + y; }
int subtract(int x, int y) { return x - y; }
int multiply(int x, int y) { return x * y; }
int divide(int x, int y) { return x / y; }

std::map<std::string, int (*)(int, int)> opFuncMap = {
            {"+", add},
            {"-", subtract},
            {"*", multiply},
            {"/", divide}
        };

使用std::function之后,可以统一类型

也就是说,在map模板使用function之后,在初始化时,不仅可以使用函数指针进行初始化,还可以使用仿函数和lambda表达式进行初始化。这时,这三种方式哪个写起来比较方便,就可以选择写哪个:

int subtract(int x, int y) { return x - y; }

struct Divide {
    int operator()(int x, int y) const { return x / y; }
};

class Solution {
public:
    int evalRPN(vector<string>& tokens) {
        // 使用map容器和function,将操作符和其操作映射起来
        std::map<std::string, std::function<int (int, int)>> opFuncMap = {
            {"+", [](int x, int y) { return x + y ;}},
            {"-", subtract}, // 这里使用函数指针
            {"*", [](int x, int y) { return x * y ;}},
            {"/", Divide() } // 这里使用仿函数
        };
        
        stack<int> st;
        for(auto& e: tokens)
        {
            // 如果是操作符
            if(opFuncMap.count(e))
            {
                int rightNum = st.top();
                st.pop();
                int leftNum = st.top();
                st.pop();
                st.push(opFuncMap[e](leftNum, rightNum));
            }
            else // 如果是操作数
            {
                // 操作数转化成整型后,入栈
                st.push(stoi(e));
            }
        }
        return st.top();
    }
};

std::bind

std::bind是一个函数适配器,用于将一个可调用对象(如函数、函数指针、Lambda表达式等)与其参数绑定在一起。它返回一个新的可调用对象,可以在稍后的时间调用。通过std::bind,可以简化函数调用的过程,特别是当某些参数已经确定时。

语法规则

std::bind头文件也在<functional> 中。

基本定义

#include <functional>

auto 新函数名 = std::bind(可调用对象, 参数1, 参数2, ..., std::placeholders::_1, ...);

std::placeholders 使用

std::placeholdersstd::bind 提供的一个命名空间,它包含了一组占位符 _1, _2, _3, ...,用于表示函数参数的位置。在绑定函数时,可以使用这些占位符来指定哪些参数将在稍后调用时被提供。

  • std::placeholders::_1 表示第一个参数,_2 表示第二个参数,以此类推。
  • 可以使用 std::placeholders 允许在新的可调用对象中保留某些参数,动态地在调用时传递。
  • std::placeholders还可以更改传入参数的顺序。
auto pfunc = std::bind(func1, std::placeholders::_2, std::placeholders::_1); // 交换参数位置
pfunc(1, 2); // 输出: 2 1

示例:

#include <iostream>
#include <functional> // 包含 std::bind

void add(int a, int b)
{
    std::cout << " a = " << a << ", b = " << b << std::endl;
    std::cout << "Sum: " << a + b << std::endl;
}

int main() {
    // 将函数 add 的第一个参数绑定为 10
    auto boundAdd = std::bind(add, 10, std::placeholders::_1);

    // 调用 boundAdd,只需要提供第二个参数
    boundAdd(5); // 输出: Sum: 15
    boundAdd(20); // 输出: Sum: 30

    return 0;
}

运行结果:

 a = 10, b = 5
Sum: 15
 a = 10, b = 20
Sum: 30

使用场景

应用一:绑定普通函数

int add(int a, int b) {
    return a + b;
}

auto boundAdd = std::bind(add, 5, std::placeholders::_1); // 绑定第一个参数为5
int result = boundAdd(3); // 等价于 add(5, 3),输出: 8

应用二:绑定Lambda 表达式

auto lambdaAdd = [](int a, int b) { return a + b; };
auto boundLambdaAdd = std::bind(lambdaAdd, 10, std::placeholders::_1); // 绑定第一个参数为10
int result = boundLambdaAdd(5); // 等价于 lambdaAdd(10, 5),输出: 15

应用三:绑定成员函数

当我们使用std::bind来绑定类的成员函数时,首先要理解成员函数的调用方式。成员函数需要一个指向对象的指针(或引用)来调用,因此在绑定时必须提供该对象。

绑定成员函数的语法

std::bind的基本语法如下:

std::bind(&ClassName::MemberFunction, &objectInstance, arg1, arg2, ...);
  • &ClassName::MemberFunction:指定要绑定的成员函数。
  • &objectInstance:指定调用成员函数的对象实例,也可以使用匿名对象(ClassName())。
  • arg1, arg2, ...:绑定的参数,如果有的话。

示例代码

class MyClass 
{
public:
    void display(int x, int y) 
    {
        std::cout << "X: " << x << ", Y: " << y << std::endl;
    }
};

int main() 
{
    MyClass obj;

    // 绑定 display 成员函数,将 y 固定为 20
    auto boundDisplay = std::bind(&MyClass::display, &obj, std::placeholders::_1, 20);

    // 调用绑定函数,只需提供 x 参数
    boundDisplay(10); // 输出: X: 10, Y: 20
    boundDisplay(30); // 输出: X: 30, Y: 20

    return 0;
}

逐步解析示例

  1. 定义类和成员函数:定义了一个类MyClass,它包含一个名为display的成员函数,该函数接受一个整数参数并输出该值。

  2. 创建实例:在main函数中,创建了MyClass的一个实例obj

  3. 绑定成员函数

    • 使用std::bind绑定了display成员函数,并将obj作为调用对象。
    • std::placeholders::_1用于占位,表示调用时会传入的第一个参数。
  4. 调用绑定函数: 调用boundDisplay并传递参数boundDisplay(value),实际调用obj.display(value, 20)

注意事项

  • 绑定常量成员函数: 如果要绑定一个const成员函数,可以用相同的方法,但对象实例需要是const的。
  • 指针和引用: 确保对象实例在调用期间有效,避免悬空指针或引用。
  • 占位符: 确保使用std::placeholders来指明参数位置。

补充: 使用Lambda替代 std::bind

在现代C++中,使用Lambda表达式通常会更加直观和简洁。例如,上面的boundDisplay可以用Lambda替代:

auto boundDisplay = [&](int x) { obj.display(x, 20); };

今天的分享就到这里了,如果,你感觉这篇博客对你有帮助的话,就点个赞吧!感谢感谢……


原文地址:https://blog.csdn.net/2201_75479723/article/details/142532922

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!