[C++11] 包装器 : function 与 bind 的原理及使用

文章目录

  • `function`
    • [`std::function` 的基本语法](#std::function 的基本语法)
    • [使用 `std::function` 包装不同的可调用对象](#使用 std::function 包装不同的可调用对象)
    • [`function`包装普通成员函数为什么要传入 `this` 指针参数?](#function包装普通成员函数为什么要传入 this 指针参数?)
    • [例题 :150. 逆波兰表达式求值 - ⼒扣(LeetCode)](#例题 :150. 逆波兰表达式求值 - ⼒扣(LeetCode))
  • `bind`
    • [`std::bind` 的基本语法](#std::bind 的基本语法)
    • [`std::bind` 参数的顺序调整与绑定](#std::bind 参数的顺序调整与绑定)
  • [`std::function` 和 `std::bind` 的实际应用](#std::functionstd::bind 的实际应用)

function

std::function 是⼀个类模板,也是一个通用的、多态函数包装器,用于存储可调用对象。函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同,<font style="color:rgb(31,35,41);">std::function</font>的优势就是统⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型。

<font style="color:rgb(31,35,41);">std::function</font> 的实例对象可以包装存储其他的可以调⽤对象,包括函数指针、仿函数、 lambda 、 bind 表达式等,存储的可调⽤对象被称为 std::function 的*⽬标* 。若 std::function 不含⽬标,则称它空。调空则抛出 std::bad_function_call 异常。


<font style="color:rgb(31,35,41);">function</font>被定义<font style="color:rgb(31,35,41);"><functional></font>头⽂件中:

std::function 的基本语法

cpp 复制代码
#include <functional>

template <class T>
class function; // 未定义的模板类

template <class Ret, class... Args>
class function<Ret(Args...)>; // 以返回类型和参数类型列表定义模板
function<返回类型(可调用对象的参数类型1,参数类型2,...)>  对象 = 可调用对象;
// int add(int a, int b)
// function<int(int, int)> func1 = add;

使用 std::function 包装不同的可调用对象

以下示例展示了 std::function 包装普通函数、仿函数、lambda 表达式、类静态成员函数和普通成员函数的用法。

cpp 复制代码
#include<functional>
#include<iostream>
using namespace std;

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

struct Functor
{
public:
	int operator() (int a, int b)
	{
		return a + b;
	}

};

class Plus
{
public:
	Plus(int n = 10)
		:_n(n)
	{}

	static int plusi(int a, int b)
	{
		return a + b;
	}

	double plusd(double a, double b)
	{
		return (a + b) * _n;
	}

private:
	int _n = 0;
};

int main()
{
	// 包装各种可调⽤对象
	function<int(int, int)> f1 = f; // 普通函数
	function<int(int, int)> f2 = Functor(); // 仿函数
	function<int(int, int)> f3 = [](int a, int b) {return a + b; }; // lambda

	cout << f1(1, 1) << endl;
	cout << f2(1, 1) << endl;
	cout << f3(1, 1) << endl;

	// 包装静态成员函数
	// 成员函数要指定类域并且前⾯加&才能获取地址
	function<int(int, int)> f4 = &Plus::plusi;
	cout << f4(1, 1) << endl;

	// 包装普通成员函数
	// 普通成员函数还有⼀个隐含的 this 指针参数,所以绑定时传对象或者对象的指针过去都可以
	function<double(Plus*, double, double)> f5 = &Plus::plusd;
	Plus pd;
	cout << f5(&pd, 1.1, 1.1) << endl;

	function<double(Plus, double, double)> f6 = &Plus::plusd;
	cout << f6(pd, 1.1, 1.1) << endl;
	cout << f6(pd, 1.1, 1.1) << endl;

	function<double(Plus&&, double, double)> f7 = &Plus::plusd;
	cout << f7(move(pd), 1.1, 1.1) << endl;
	cout << f7(Plus(), 1.1, 1.1) << endl;

	return 0;
}

在C++中,普通成员函数的调用与静态成员函数或普通的非成员函数不同,因为它隐含了一个 this 指针参数。这是由于普通成员函数总是绑定到某个对象实例,因此在调用时需要知道具体是哪个对象调用了该函数。

function包装普通成员函数为什么要传入 this 指针参数?

当我们使用 std::function 来包装普通成员函数时,普通成员函数的签名实际上是:

cpp 复制代码
ReturnType (ClassType::*)(ParamTypes...)

这个签名表示该成员函数属于特定的类,因此它并不完全等同于普通函数。每个普通成员函数的调用实际上是通过一个特定的对象调用的,而对象的地址(this 指针)在函数调用时必须传入。

在普通成员函数的调用中:

  • this 指针作为隐式参数,指向调用函数的对象实例。
  • std::function 包装这种成员函数时需要显式地传入 this 指针,以便知道调用时该成员函数应该作用于哪个对象实例。

例如,假设有如下成员函数:

cpp 复制代码
double Plus::plusd(double a, double b) {
    return a + b;
}

在使用 std::function 包装时,由于 plusd 是非静态成员函数,需要显式传入一个 Plus 实例(对象)或该实例的指针作为 this。可以通过传入对象指针 Plus*,或者直接传递一个对象实例 Plus 来间接实现这种绑定。

传入对象指针与传入对象实例的区别

  1. 传入对象指针(例如 Plus* :这种情况下,std::function 会调用成员函数时使用传入的指针来绑定 this。先创建一个Plus实例,然后传入该实例的地址。
cpp 复制代码
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus pd;
f5(&pd, 1.1, 1.1);

在这里,f5(&pd, 1.1, 1.1); 调用时,&pd 指向的对象作为 this 指针传入。

  1. 传入对象实例(例如 Plus :当传入一个对象时,C++ 会复制这个对象并为其分配一个独立的内存空间,然后将其临时地址传给 this,使得 this 指向该副本。
cpp 复制代码
function<double(Plus, double, double)> f6 = &Plus::plusd;
Plus pd;
f6(pd, 1.1, 1.1);

这样,调用 f6(pd, 1.1, 1.1); 会将对象 pd 复制一份传入,使得成员函数 plusdthis 指针指向该副本。

传入对象实例的优缺点:

  • 优点:传入对象实例更加直观,代码上不需要关注指针。
  • 缺点 :会产生对象的拷贝(除非对象使用 std::move),因此可能有额外的开销。如果对象较大或者包含较多成员变量,拷贝代价较高。
  1. **直接传入一个匿名对象:**减少拷贝次数
cpp 复制代码
function<double(Plus, double, double)> f6 = &Plus::plusd;
f6(Plus(), 1.1, 1.1);

当我们使用 Plus() 作为匿名对象传入时:

  • 匿名对象在调用的那一行直接生成,不需要从其他地方复制数据。
  • std::function 中的 f6 会直接使用这个匿名对象,临时对象的生命周期刚好覆盖整个调用过程,调用结束后立即销毁。

这样做可以在保证代码清晰 的同时避免多余的拷贝 。所以,传入 Plus() 是一种优化写法,尤其适合对象初始化开销较大、但不需要持续存在的情况。

所以在包装匿名对象时一般推荐使用该种方法。

例题 :150. 逆波兰表达式求值 - ⼒扣(LeetCode)

. - 力扣(LeetCode)

cpp 复制代码
// 使⽤map映射string和function的⽅式实现
// 这种⽅式的最⼤优势之⼀是⽅便扩展,假设还有其他运算,我们增加map中的映射即可
class Solution {
public:
    int evalRPN(vector<string>& tokens) 
    {
        stack<int> st;

        // Fixing the map initialization
        map<string, function<int(int, int)>> opFuncMap = {
            {"+", [](int x, int y){ return x + y; }},
            {"-", [](int x, int y){ return x - y; }},
            {"*", [](int x, int y){ return x * y; }},
            {"/", [](int x, int y){ return x / y; }}
        };

        for(auto str : tokens)
        {
            if(opFuncMap.count(str))
            {
                int top = st.top();
                st.pop();
                int next_top = st.top();
                st.pop();

                int ret = opFuncMap[str](next_top, top);
                st.push(ret);
            }
            else // Handling operand case
            {
                st.push(stoi(str));
            }
        }

        return st.top();
    }
};

<font style="color:rgb(31,35,41);">bind</font>

bind<functional>头文件中,std::bindfunction类似,也是⼀个函数模板,同时是一个函数适配器,用于将可调用对象的参数进行绑定或者参数顺序的调整,返回一个新的可调用对象(本质是一个仿函数对象)。

std::bind 可以调整原有函数的参数个数和顺序,适配更为灵活的调用方式。它广泛用于实现函数的"占位符"特性和简化代码的参数传递。

cpp 复制代码
#include <functional>

template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);

std::bind 的基本语法

cpp 复制代码
auto newCallable = bind(callable,arg_list); 
// newCallable 为绑定后的可调用对象 类型由 auto 推导
// callable 是要进行绑定或者进行调整参数传递顺序的函数
// arg_list 是 callable 进行具体调整的参数列表(可以包括占位符或绑死的参数)

其中<font style="color:rgb(31,35,41);">newCallable</font>本⾝是⼀个可调⽤对象,<font style="color:rgb(31,35,41);">arg_list</font>是⼀个逗号分隔的参数列表,对应给定的<font style="color:rgb(31,35,41);">callable</font>的参数。当我们调⽤<font style="color:rgb(31,35,41);">newCallable</font>时,<font style="color:rgb(31,35,41);">newCallable</font>会调⽤<font style="color:rgb(31,35,41);">callable</font>,并传给它<font style="color:rgb(31,35,41);">arg_list</font>中的参数。

std::bind 参数的顺序调整与绑定

顺序调整

std::bind 中,通过 placeholders 命名空间可以使用 _1_2 等占位符表示绑定的函数参数。

cpp 复制代码
using namespace placeholders; // 将占位符全部展开

这些占位符用于定义生成的可调用对象中参数的位置,例如 _1 表示第一个参数, _2 表示第二个参数,以此类推。

cpp 复制代码
using placeholders::_1;
using placeholders::_2;
// using placeholders::_3;

int Sub(int a, int b)
{
	return (a - b) * 10;
}

auto sub1 = bind(Sub, _1, _2); 
// 传入Sub函数,_1 _2表示使用新的可调用对象sub1时传入的第一个和第二个参数
cout << sub1(10, 5) << endl; // Sub(10, 5);

auto sub2 = bind(Sub, _2, _1); 
// 传入Sub函数,_1 _2表示使用新的可调用对象sub1时传入的第二个和第一个参数
cout << sub2(10, 5) << endl; // Sub(5, 10);

参数的绑定

如果想让某个参数的值进行绑定,就在该参数位置上传入值即可,之后如果有传入参数需要可以继续按照占位符当前个数继续进行填写。 **_1, _2 ...**仅表示绑定后的新可调用对象传入的参数及顺序。

cpp 复制代码
// 调整参数个数 (常⽤)
auto sub3 = bind(Sub, 100, _1);
cout << sub3(5) << endl;

auto sub4 = bind(Sub, _1, 100);
cout << sub4(5) << endl;

// 分别绑死第1 2 3个参数
auto sub5 = bind(SubX, 100, _1, _2);
cout << sub5(5, 1) << endl;
auto sub6 = bind(SubX, _1, 100, _2);
cout << sub6(5, 1) << endl;
auto sub7 = bind(SubX, _1, _2, 100);
cout << sub7(5, 1) << endl;

// function<返回值类型(传入的各个参数类型 ,...)>
// 成员函数对象进⾏绑死,就不需要每次都传递了
function<double(Plus&&, double, double)> f6 = &Plus::plusd;
Plus pd;
cout << f6(move(pd), 1.1, 1.1) << endl;
cout << f6(Plus(), 1.1, 1.1) << endl;

// 可以利用 bind 绑死function需要包装的函数,将Plus::plusd函数包装后要传入的this部分绑死
function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f7(1.1, 1.1) << endl;

std::functionstd::bind 的实际应用

  1. 函数指针和回调函数

std::functionstd::bind 的组合可以让回调函数的参数更具灵活性。例如,在实现事件回调时可以使用 std::function 存储回调函数,并用 std::bind 将具体参数与回调绑定。

  1. 函数作为容器的元素

在需要存储不同类型的可调用对象的容器中,使用 std::function 是一个最佳选择。利用 std::function 可以将不同类型的函数包装在一个容器中统一存储,并在需要时调用。

  1. 参数绑定和延迟调用

std::bind 可以用于创建参数部分固定的函数对象,从而减少函数调用时的参数传递。这种方式在处理回调和异步编程中非常有用。


结论

C++11 提供的 std::functionstd::bind 为现代 C++ 编程带来了极大的便利。std::function 允许将不同类型的可调用对象进行统一存储和操作,简化了代码结构。而 std::bind 则可以灵活地调整函数参数和调用方式,为开发者提供了高效、简洁的代码编写方式。在日常开发中,合理运用这两个包装器可以显著提高代码的可读性和可维护性。

相关推荐
秋恬意3 分钟前
Java NIO 核心知识总结
java·开发语言·nio
程序猿阿伟7 分钟前
《原子操作:程序世界里的“最小魔法单位”解析》
java·数据库·c++
不7夜宵8 分钟前
Golang defer关键字
开发语言·后端·golang
一丝晨光14 分钟前
Chrome和Chromium的区别?浏览器引擎都用的哪些?浏览器引擎的作用?
前端·c++·chrome·webkit·chromium·blink·trident
马红权15 分钟前
qtabwidget qtablewidget显示excel工作表内容(极简excel viewer)
开发语言·python·算法
PythonFun17 分钟前
Excel中批量替换字符大PK:Excel VS. Python
开发语言·python·excel
PythonFun24 分钟前
Python调用API翻译Excel中的英语句子并回填数据
开发语言·python·excel
曹天骄38 分钟前
mac怎么看当前终端是zsh还是bash
开发语言·macos·bash
凤枭香43 分钟前
数字图像处理(c++ opencv):图像复原与重建-常见的滤波方法--统计排序滤波器
c++·图像处理·opencv·计算机视觉
凤枭香44 分钟前
数字图像处理(c++ opencv):图像复原与重建-常见的滤波方法--自适应滤波器
开发语言·c++·图像处理·opencv