bind绑定器与funciton函数对象实现原理(超详细)

本文介绍了C++中bind1st和bind2nd作为绑定器的作用,以及它们的底层原理,展示了如何将二元函数对象转换为一元函数对象。接着,讨论了function函数对象的重要性,特别是在封装不同类型的函数和函数对象时的灵活性。最后,探讨了lambda表达式的功能和优势,以及其在捕获外部变量和表达式简化方面的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C++中引入bind绑定器和function函数对象

一步一步来,先学习下bind1st和bind2st的底层原理,才好理解后面的bind绑定器和function函数对象的底层原理。

bind绑定器

bind1st和bind2nd什么时候会用到

介绍:bind1st是一个辅助模板函数,它创建一个适配器,通过将二元函数的第一个参数绑定为指定值,将二元函数对象转换为一元函数对象。
bind2nd类似,很好理解。

先举个实例来说明绑定器具体有什么应用场景:

#include <iostream>
#include <vector>
#include <ctime>
#include <functional>
#include <algorithm>
using namespace std;

template<typename Container>
void showContainer(Container& con){
    for(auto e: con)
        cout<< e << " ";
    cout << endl;

}

int main(){
    srand(time(nullptr));
    vector<int> vec;
    for(int i = 0; i < 10; ++i)
        vec.push_back(rand() % 100 + 1);
    // sort(vec.begin(), vec.end()); // 默认升序
    // showContainer(vec);
    sort(vec.begin(), vec.end(), greater<int>()); // 降序
    showContainer(vec);
    /* 
    这个时候我想把77 插入到降序的vec中,也就是用find_if, 它的第三个参数需要一个一元函数对象。
    auto iter = find_if(vec.begin(), vec.end(), ###);
    如果我们想用greater<int> 来实现,是不行的,因为greater<int>是一个二元函数对象,它接受两个参数。
    怎么办呢?这时就可以用到绑定器,把77绑定到greater<int>的第一个参数,而后返回一个一元函数对象,作为find_if的第三个参数。
    当遇到第一个77 > elem 的元素时,返回elem的迭代器,然后把77插入该位置即可。
    */
    auto iter = find_if(vec.begin(), vec.end(), bind1st(greater<int>(), 77));
    vec.insert(iter, 77);
    showContainer(vec);
}

bind1st和bind2nd底层原理

以下实现了my_find_if和my_bind1st函数。

#include <iostream>
#include <vector>
#include <ctime>
#include <functional>
#include <algorithm>
using namespace std;

template<typename Container>
void showContainer(Container& con);

// 手动实现my_find_if有助于理解绑定器
template<typename Iterator, typename Compare>
Iterator my_find_if(Iterator first, Iterator last, Compare comp){
    for(; first < last; ++first){
        // 可见 find_if 需要一个一元函数对象
        if(comp(*first)) return first;
    }
}


// 手动实现一个my_bind1st

// 首先要看懂这是个啥? 这是个一元函数对象类类型
template<typename Compare, typename T>
class _mybind1st{
public:
    _mybind1st(const Compare& comp, T val): _comp(comp), _val(val){}
    bool operator()(T& second){
        return _comp(_val, second);
    }

private:
    Compare _comp;
    T _val;

};

template<typename Compare, typename T>
_mybind1st<Compare, T> my_bind1st(const Compare& comp, const T& val){
    return _mybind1st<Compare, T>(comp, val);
}


int main(){
    srand(time(nullptr));
    vector<int> vec;
    for(int i = 0; i < 10; ++i)
        vec.push_back(rand() % 100 + 1);
    // sort(vec.begin(), vec.end()); // 默认升序
    // showContainer(vec);
    sort(vec.begin(), vec.end(), greater<int>()); // 降序
    showContainer(vec);
 
    auto iter = my_find_if(vec.begin(), vec.end(), my_bind1st(greater<int>(), 77));
    vec.insert(iter, 77);
    showContainer(vec);

}

template<typename Container>
void showContainer(Container& con){
    for(auto e: con)
        cout<< e << " ";
    cout << endl;

}

首先介绍一下这个类:_mybind1st
_mybind1st是个一元函数对象类类型, 这一点可以从重载的函数调用运算符可以看出,它只接受一个参数second。
其次它有两个成员变量,一个是Compare类型的_comp,一个是准备绑定到bind1st的第一个位置上的参数。
因为我们在类里存储了二元函数对象,那么在重载的函数调用运算符中就可以调用这个它,并且可以先将事先传过来的参数_val“作为默认形参”绑定到_comp的第一个参数上,然后调用运算符接受一个参数作为这个一元函数对象的唯一参数。至此_mybind1st就是一个一元函数对象类型了。

为什么非要定义一个返回一元函数对象的函数my_bind1st呢?
这是因为在C++17以前,模板类是不能参数推导的,为了省去我们自己给定类模板参数的麻烦,我们先让函数把comp和val的类型推导出来,然后传给_mybind1st作为类模板参数。
举个自己传递类模板参数的例子:

// 把这一行
auto iter = my_find_if(vec.begin(), vec.end(), my_bind1st(greater<int>(), 77));
// 改成
auto iter = my_find_if(vec.begin(), vec.end(), _mybind1st<greater<int>, int>(greater<int>(), 77));
// 也是可以运行的

function函数对象

function函数对象什么时候会用到

首先说一下为什么会用到function函数对象。
因为不论是绑定器,函数对象,lambda表达式,它们只能用在一条语句中,如果我们想把它们的类型留下来,那就要用到function函数对象。

function函数对象包装函数

先举个简单例子:

void hello1(){
    cout<<"hello world!"<<endl;
}
void hello2(string str){
    cout<<str<<endl;
}
int main(){
    // void(string) 是一个函数类型,不是函数指针类型
    function<void(string)> func2(hello2);
    func2("hello world!"); // func1.operator(string) => hello2(string)
}

这个简单的例子使用函数指针也可以实现,接下来举个例子说明function函数对象的必要性:


int main(){
    // 如果我这么写
    void(*pfunc1)() = hello1; // 正确

	// 如果尝试使用函数指针绑定到函数对象上:
    // error: 不存在从 "std::_Bind<void (*(const char *))(std::string str)>" 到 "void (*)()" 的适当转换函数C/C++(413)
    void(*pfunc2)()  = bind(hello2, "hello world!"); 
    
    pfunc1();
    pfunc2(); // 报错
}

可以发现函数指针只能指向不同函数,不能指向函数对象。

function函数对象包装lambda表达式

int main(){
    function<int(int, int)> func3([](int a, int b)->int{ return a + b; });
    cout << func3(1, 2) << endl; // 3
}

function函数对象包装成员函数

class Test{
public:
    void hello(string str){  // 通过函数指针调用:void (Test::*pfunc)(string)
    cout << str << endl;
}
};

int main(){
    function<void(Test*, string)> func4(&Test::hello);
    Test t;
    func4(&t, "call Test::hello");
}
  • 用函数类型实例化function;
  • 通过function调用operator的时候,需要根据函数类型传入相应的参数。

举个实际点的使用场景

假设要做一个图书管理系统:

int main(){
    int choice = 0;
    while(1){
        cout << "-----------------" << endl;
        cout<< "1.查看所有书籍信息" << endl;
        cout<< "2.借书" << endl;
        cout<< "3.还书" << endl;
        cout<< "4.查询书籍" << endl;
        cout<< "5.注销" << endl;
        cout<< "请选择";
        cin >> choice;
        switch(choice){
            case 1:
                break;
            case 2:
                break;
            case 3:
                break;
            case 4:
                break;
            case 5:
                break; 
            default:
                break;
        }

    }
}

这样写好不好呢?当然不好,因为当我们增加、删除某个功能的时候,这里的业务代码整个就要重新修改、编译,不符合设计模式中的开闭原则,难以维护。
这个时候就要引出function函数对象了。

我们这样来设计这个功能,首先来新建一个action.h,里面包含一个Action类和menu函数:

// action.cpp
#include <iostream>
#include <functional>
#include <string>
#include <unordered_map>
using namespace std;


class Action{
public:
    Action(){
        actionMap.insert({1, doShowAllBooks});
        actionMap.insert({2, doBorrow});
        actionMap.insert({3, doBack});
        actionMap.insert({4, doQueryBooks});
        actionMap.insert({5, doLoginOut});

    }
    function<void()> getSolution(int choice){
        auto it = actionMap.find(choice);
        if(it == actionMap.end()){
            return Error;
        }
        else return actionMap[choice];
    }
    static void doShowAllBooks(){ cout << "查看所有书籍信息" << endl; }
    static void doBorrow(){ cout << "借书" << endl; }
    static void doBack(){ cout << "还书" << endl; }
    static void doQueryBooks(){ cout << "查询书籍" << endl; }
    static void doLoginOut(){ cout << "注销" << endl; }
    static void Error(){ cout << "输入的选项无效,请重新输入!" << endl; }


private:
    unordered_map<int, function<void()>> actionMap;
};

inline void menu(){
    cout << "-----------------" << endl;
    cout<< "1.查看所有书籍信息" << endl;
    cout<< "2.借书" << endl;
    cout<< "3.还书" << endl;
    cout<< "4.查询书籍" << endl;
    cout<< "5.注销" << endl;
    cout<< "请选择:";
}

我们的主业务源文件就变成:

// main.cpp
#include <iostream>
#include <functional>
#include <string>
#include <unordered_map>
#include "action.h"
using namespace std;
int main(){
    Action action;
    int choice = 0;
    while(1){
        menu();
        cin >> choice;
        auto solution = action.getSolution(choice);
        solution();
    }
}

经过这样的设计,当我们有共模块的增删改需求时,我们只需要修改action类和菜单函数,而不需要去动主业务模块,它已经完全闭合。
有人说不用function函数对象,用函数指针也是可以实现这样的设计的,是的确实。但是如果功能模块使用的是bind绑定器,或者lambda函数对象,函数指针就无能为力了。

funtion的实现原理

一个简单的function函数对象实现:

#include <iostream>
#include <functional>
#include <string>
#include <unordered_map>

using namespace std;

void hello(string str){
    cout<<str<<endl;
}

template<typename Fty>
class myfunction{};


template<typename R, typename A1>
class myfunction<R(A1)>{
public:
    using PFUNC = R(*)(A1);
    myfunction(PFUNC pfunc): _pfunc(pfunc){}
    R operator()(A1 arg){
        return _pfunc(arg);
    }
private:
    PFUNC _pfunc;
};


int main(){
    myfunction<void(string)> func(hello);
    func("hello");
}

使用可变模板参数实现接收任意参数。
```c++
template<typename R, typename... A>
class myfunction<R(A...)>{
public:
    using PFUNC = R(*)(A...); 
    myfunction(PFUNC pfunc): _pfunc(pfunc){}
    R operator()(A... arg){
        return _pfunc(arg...);
    }
private:
    PFUNC _pfunc;
};

可以看到其内部就是用函数指针来保存包装的函数,但上面说了,函数指针是不能指向bind绑定器等一类的函数对象的,所以怎么办呢? 这里我也不知道,可能要去看function的源码才能知道如何实现的,function源码详解,挺深的以后再看。

lambda表达式

首先说一下lambda表达式的优势。
首先说一下函数对象的缺点,函数对象一般用在泛型算法中的参数传递,包括比较性质的/自定义操作。
那么为了实现这些操作,就要定义一个仿函数的类型,但也许用完之后,就再也用不到这个类型了,显然易见定义一个这样的类型是没有必要的。
因此lambda表达式就应运而生了,我们不需要新定义一个类型,直接使用lambda表达式就可以实现上述的需求。

lambda表达式的实现原理

先看一下lambda表达式的语法:

[捕获外部变量](形参列表)->返回类型{函数体}
#include <iostream>

using namespace std;

template <typename T = void> // 模板类型参数对应lambda的返回类型
class TestLambda01{
public:
    TestLambda01(){} // 构造函数 对应lambda表达式的捕获列表
    T operator()() const { // 函数调用运算符的形参列表对应lambda表达式的形参列表
        cout << "hello world!" << endl; // 函数体对应lambda表达式的函数体
    }
};

template <typename T = int>
class TestLambda02{
public:
    TestLambda02(){} 
    T operator()(int a, int b) const {
        return a + b; 
    }
};

int main(){
    auto func1 = []()->void { cout << "hello world!" << endl;};
    func1(); // hello world!
    TestLambda01<>()(); // hello world!

    auto func2 = [](int a, int b)->int { return a + b;};
    cout << func2(1, 2)<< endl; // 3
    cout << TestLambda02<>()(1, 2) << endl; // 3
}

接下来看下捕获列表的使用方法:

  • []:表示不捕获任何外部变量;
  • [=]:以传值的方式捕获外部的所有变量
  • [&]:以捕获的方式捕获外部的所有变量
  • [this]:捕获外部的this指针(this指针只能值捕获不能引用捕获)
  • [=, &a]:以传值的方式捕获除a外所有的变量,以引用方式捕获变量a
  • [a, b]:以传值的方式捕获外部变量a和b
  • [&a, &b]:以引用的方式捕获外部变量a和b

int main(){
	// lambda表达式只会捕获在定义它之前出现的变量
    auto func3 = [&]()->void { cout << a << " " << b << " " << endl;}; // error: ‘a’ was not declared in this scope
    int a = 1, b = 2;
    // auto func4 = []()->void { cout << a << " " << b << " " << endl;}; // error: ‘a’ is not captured 、‘b’ is not captured
    auto func4 = [&]()->void { cout << a << " " << b << " " << endl;};
    func4(); // 1 2 
    {
        func4(); // 1 2 
        auto func5 = [&a, &b]()->void { cout << a << " " << b << " " << endl;}; // 1 2
        func5(); // 1 2
    }

}

值传递不能够修改成员变量,因为函数调用运算符是个const方法:

#include <iostream>

using namespace std;

template <typename T = void>
class TestLambda03{
public:
    TestLambda03(int a, int b):ma(a), mb(b){} 
    T operator()() const {
        swap(ma, mb); // mismatched types ‘std::__cxx11::basic_string<_CharT, _Traits, _Alloc>’ and ‘const int’
    }
private:
    int ma;
    int mb;
};

int main(){
    int a = 1, b = 2;
    // auto func6 = [=]()->void { swap(a, b);}; // mismatched types ‘std::__cxx11::basic_string<_CharT, _Traits, _Alloc>’ and ‘const int’

    TestLambda03<>(a, b)();
}

如果想要修改:

#include <iostream>

using namespace std;

template <typename T = void>
class TestLambda03{
public:
    TestLambda03(int a, int b):ma(a), mb(b){} 
    T operator()() const {
        swap(ma, mb); 
    }
private:
    mutable int ma;
    mutable int mb;
};

int main(){
    int a = 1, b = 2;
    auto func6 = [=]()mutable ->void { swap(a, b);}; 
    cout << a << " " << b << endl; // 值传递,没有改变
    TestLambda03<>(a, b)();
}

如果是引用捕获,则不需要加mutable

因为在常函数中使用引用的话,改变的不是引用本身,而是改变引用的变量。

为什么只能值捕获this指针不能引用捕获this指针

首先,编译器是拒绝引用捕获this指针的,这里我举个简单的例子来说明引用捕获this指针会有哪些风险:
这段代码就类比于引用捕获,ra引用了This。本质上相当与ra是一个指向This 的指针常量,那么当This指向的内存被释放后,ra还是可以使用的,因为它还是保存了一个地址,只是内存被释放了,所以输出一个随机值。但是当This被置空后,ra就变成了悬空引用,它指向的内存被释放了,并且还被置了空。解引用就会产生未定义的行为。


#include<iostream>
#include<functional>

int main() {
    int* This = new int(5);
    int*&  ra = This;
    delete This;
    std::cout << *ra << std::endl; // 1431655786
    This = nullptr;
    
    std::cout << *ra << std::endl; // 未定义
    return 0;
}

而采用值捕获的话,pa是一个指针变量,它保存了了This保存的地址,This指向的内存被释放了,那么pa指向的内存自然也就被释放了;但是This被置了空,却不影响pa,因为它们是两个不同的对象。尽管不会产生未定义的行为,但是仍可能访问到无效的内存。
因此,必须使用值捕获的原因是,避免引用捕获this指针造成的悬空引用,进而产生的未定义行为。

#include<iostream>
#include<functional>

int main() {
    int* This = new int(5);
    int*  pa = This;
    delete This;
    std::cout << *pa << std::endl; // 1431655786
    This = nullptr;
    
    std::cout << *pa << std::endl; // 1431655786
    return 0;
}

lambda表达式的应用实践

既然lambda表达式只能使用在语句当中,如果想跨语句使用之前定义好的lambda表达式,怎么办?用什么类型来表示lambda表示式?当然使用function类型来表示函数对象的类型了。

假设要实现一个计算器:

#include <iostream>
#include <unordered_map>
#include <functional>
using namespace std;

int main(){
   // 假设我要用一个map来存储消息类型和对应的回调函数
   // 我现在知道要用lambda表达式,但我现在还不知道lambda表达式的具体实现,要用到lambda表达式的类型
   unordered_map<int, function<int(int, int)>> calculateMap;
   calculateMap[1] = [](int a, int b)->int {return a + b;};
   calculateMap[2] = [](int a, int b)->int {return a - b;};
   calculateMap[3] = [](int a, int b)->int {return a * b;};
   calculateMap[4] = [](int a, int b)->int {return a / b;};

    cout << "10 + 20 = " << calculateMap[1](10, 20) << endl;
}

智能指针删除器:

#include <iostream>
#include <unordered_map>
#include <functional>
#include <memory>

using namespace std;

int main(){
   // 对于智能指针自定义删除器,如果打开的是一个文件,那么删除器显然要调用fclose
   // 此时新定义一个类型是完全没必要的,完全可以使用lambda表达式
    unique_ptr<FILE, function<void(FILE*)>> ptr1(fopen("data.txt", "w"), [](FILE* fp){ fclose(fp); });
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值