绑定器和函数对象的实现原理

6 篇文章 0 订阅
4 篇文章 0 订阅
  • 目录
    • C++ STL中的绑定器
    • C++11中的bind和function
    • lamdba表达式使用及其原理

绑定器和函数对象

如果一个对象中重载了operator(),称这个对象是一个函数对象。因为它可以像函数被使用。而绑定器可以将函数中的一个形参变量绑定成一个固定的值。
直白点说,函数对象就是一个对象调用operator(),但是看起来就像是直接调用普通函数。而绑定器就是可以对函数做一层封装,原本要调用a+b需要调用add(a, b),但是如果我就只要做10加上另一个数字的假发,那么绑定器内部就是add(10, b),然后用户只需要传入一个数字即可。

bind1st和bind2nd的使用

bind1st:将函数的第一个参数绑定成一个固定的值。
bind2sn:将函数的第二个参数绑定成一个固定的值。

使用find_id举例

find_if的功能是传入一个比较函数和容器的查询范围,返回在这个容器中第一个满足比较函数条件的值。但是比较函数只能接受一个参数。

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

using namespace std;

int main()
{
    vector<int> nums;
    for (int i = 1; i <= 20; i ++)
    {
        nums.push_back(i);
    }
    /**
     * 找到nums中第一个大于10的数字
     * find_if(迭代范围, 函数对象)
     *      函数对象:operator()(const T& val) { return val > 10; } 从前往后描扫,数值<=10的数字都要跳过
     * greater: a > b 和 less: a < b都是二元函数对象,需要通过绑定器固定其中的一个参数,以做到 a > 10 或者 10 < b
     * 因此可以使用bind1st(less<int>(), 10) : 原来的是a<b,由于绑定了第一个参数就成了10 < b
     * 也可以使用bind2nd(greater<int>(), 10) : 原来是a>b,由于绑定了第二个参数就成了a > 10
     */
    auto it1 = find_if(nums.begin(), nums.end(), bind1st(less<int>(), 10));
    auto it2 = find_if(nums.begin(), nums.end(), bind2nd(greater<int>(), 10));
    cout << *it1 << ' ' << *it2 << endl;
    return 0;
}

bind1st和bind2nd的原理实现

绑定器就是对二元函数对象做了进一步的封装。

template <typename Iterator, typename Compare>
Iterator myfind_if(Iterator first, Iterator last, Compare cmp)
{
    for (; first != last; ++ first)
    {
        // 使用一元函数对象进行比较
        if (cmp(*first))
        {
            return first;
        }
    }
    return last;
}

template<typename Compare, typename T>
class _mybind1st
{
public:
    _mybind1st(Compare cmp, T val):_cmp(cmp), _val(val) {}
    // 一元函数
    bool operator()(const T& second)
    {
        // 其实这里的 一元函数 对象是通过 二元函数 实现的
        return _cmp(_val, second);
    }
private:
    // 二元函数对象
    Compare _cmp;
    // 需要绑定的值。这里是bind1st,所以_val就是固定下二元函数对象_cmp的第一个参数就是_val
    T _val;
};

/**
 * "二元函数对象" 使用 "绑定器" 成为了 "一元函数对象"
 * 所以 绑定器 是 函数对象 的一个应用
 */
template<typename Compare, typename T>
_mybind1st<Compare, T> mybind1st(Compare cmp, const T& val)
{
    // 封装一下 _mybind1st 的一元函数对象,然后直接返回
    return _mybind1st<Compare, T>(cmp, val);
}

function的使用

函数指针,函数对象,绑定器,lambda表达式都可以像函数一样被调用,但是它们的类型却不一样,并且不无法保存下来。funcional就可以统一类型,并以function的形式保存下来

void hello()
{
    cout << "hello world" << endl;
}

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

class Test
{
public:
    void print(string str)
    {
        cout << str << endl;
    }
};

int main()
{
    /**
     * 函数指针,函数对象,绑定器,lambda表达式都可以像函数一样被调用,但是它们的类型却不一样,并且不无法保存下来
     * funcional就可以统一类型,并以function的形式保存下来
     */

     // 1.封装函数/函数指针
     // 注意<>中填的是 “函数类型” ,而不是“函数指针类型“。
     function<void()> func1 = hello; // 不是function<void(*)()>
     func1(); // 相当于hello()

     function<int(int, int)> func2 = add;
     cout << func2(1, 1) << endl; // 相当于add(1, 1)

     // 2.封装lambda表达式
     function<void(string)> func3 = [](string str) {cout << str << endl;};
     func3("hello world");

     // 3.封装函数对象
     // 注意对象的成员函数有一个隐藏的参数是this,所以封装的时候也要将this传入才可以
     function<void(Test*, string)> func4 = &Test::print;
     Test t;
     func4(&t, "hello world");

    return 0;
}

leetcode150题可以用用function很好的解决问题。

150. 逆波兰表达式求值

有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
**注意 **两个整数之间的除法只保留整数部分。
可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

class Solution {
public:
    typedef long long LL;
    int evalRPN(vector<string>& tokens) {
        unordered_map<string, function<int(int, int)>> ops = {
            {"+", [](LL a, LL b){return a + b;}},
            {"-", [](LL a, LL b){return a - b;}},
            {"*", [](LL a, LL b){return a * b;}},
            {"/", [](LL a, LL b){return a / b;}}
        };
        stack<LL> sk;
        for (string& token: tokens) {
            if (ops.count(token)) {
                int b = sk.top(); sk.pop();
                int a = sk.top(); sk.pop();
                sk.push(ops[token](a, b));
            } else {
                sk.push(stoi(token));
            }
        }
        return sk.top();
    }
};

function的实现原理

function的实现原理就是通过模板将函数类型保存下来,然后将函数地址传递到function类的内部保存起来,最终通过在function中重载operator()再次调用原函数。

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

template<typename Fty>
class myfunction {};

// R是抽象的返回值类型,A1是抽象的一个形参变量类型
template <typename R, typename A1>
class myfunction<R(A1)>
{
public:
    // 模板类型的别名只能使用using,而不能使用typedef
    using PFUNC = R(*)(A1);
    // myfunction(R(*pfunc)(A1)):_pfunc(pfunc) {}
    // 等同于上面一种写法
    myfunction(PFUNC pfunc):_pfunc(pfunc) {}
    R operator()(A1 arg)
    {
        _pfunc(arg);
    }
private:
    PFUNC _pfunc;
};

int main()
{
    myfunction<void(string)> func(print);
    func("hello world");
    return 0;
}

上面这种写法的思想就是function的核心思想,即将通过function类中的operator()调用传进来的函数的地址。但是有一个问题就是如果如果函数的参数的个数是两个甚至很多,例如myfuntion<void(string, string, string, string)>,那么需要另外再写一个类,其中抽象的模板参数类型就需要有4个typename A1。而function中不是这样实现的。C++11的模板语法中允许传入一个参数包,即参数包中包含了很多的参数,例如:…arg的形式。利用这个语法就可以接收任意多个参数类型

template<typename Fty>
class myfunction {};

// 注意参数包在声明的时候要使用...A,而在展开参数包的时候要使用A...
template<typename R, typename... A>
class myfunction<R(A...)>
{
public:
    using PFUNC = R(*)(A...);
    myfunction(PFUNC pfunc):_pfunc(pfunc) {}
    // A...相当于将参数类型展开
    R operator()(A... args)
    {
        // args...表示传递任意多个参数
		// 注意:A...是类型展开,args...是不同类型的参数展开。但是其实都是将传递的任意多个参数进行展开
        _pfunc(args...);
    }
private:
    PFUNC _pfunc;
};

int main()
{
    myfunction<void(string)> func(print);
    func("hello world"); // 输出 "hello world"
	// 匹配2个参数
    myfunction<int(int, int)> func2([](int a, int b){ return a + b; });
    cout << func2(1, 1) << endl; // 输出 2
	// 还可以匹配任意多个参数
    return 0;
}

bind的使用

C++ STL中的bind1st和bind2nd只能绑定一个参数,使得二元函数对象变成一元函数对象。
C++11中的bind功能十分强大,可以绑定任意多个参数。并且对函数对象的参数个数没有要求,bind可以配合placeholder::_x占位符,如果不想绑定的参数可以通过占位符进行替代。

void print(string str ) { cout << str << endl; }
int add(int a, int b) { return a + b; }
class Test
{
public:
    void print(string str) { cout << str << endl; }
};

int main()
{
    /**
     * 使用bind可以给函数绑定参数值
     */
    bind(print, "hello world")(); // 因为已经绑定了string参数,所以直接调用即可
    cout << bind(add, 1, 2)() << endl; // 因为已经绑定了int, int参数,所以直接调用即可
    bind(&Test::print, Test(), "hello world")();

    /**
     * 如果只想要绑定函数中的部分参数,可以只传递部分参数,其余参数可以使用placeholder命名空间的占位符替代
     */
    bind(print, placeholders::_1)("hello world"); // 因为没有绑定参数,唯一的一个参数为占位符_1替代,所以需要传一个参数
    cout << bind(add, placeholders::_1, 10)(20) << endl; // 因为绑定了一个参数,而函数有两个参数,所以还需要传递一个参数

    /**
     * 如果bind和function进行配合使用,那么就可以将bind返回的函数对象保存下来了
     */
    // 注意:如果没有绑定器,func的类型应该是function<void(Test*,string)>,
    // 但是由于绑定器将Test()对象已经传入,所以只需要传递string参数即可
    function<void(string)> func = bind(&Test::print, Test(), placeholders::_1);
    func("hello world");

    return 0;
}

综合案例:使用bind和function实现一个ThreadPool线程池

实现功能注意点:无论是C++11中的thread对象创建线程还是Linux中pthread_ceate创建线程,线程运行的函数只能是C函数,而使用ThreadPool类封装的成员线程函数一定会有隐含的this指针。你可以将成员线程函数写成static的,这样就不会有this了。还有另一种做法就是通过bind将this绑定下来,这样看起来线程函数就不用传递this指针了。

class Thread
{
public:
    Thread(function<void(int)> func, int id):_func(func), _id(id)
    {}
    thread start()
    {
        // 直接调用C++11中的 "thread" 类去 "创建线程",并运行传递进来的 "线程函数"
        thread t(_func, _id);
        // 返回线程句柄
        return t;
    }
private:
    // 接收传递建立的线程函数
    function<void(int)> _func;
    int _id;
};

class ThreadPool
{
public:
    ThreadPool() = default;
    ~ThreadPool()
    {
        for (thread& td: _handler)
        {
            td.join();
        }
        for (Thread*& ptd: _pool)
        {
            delete ptd;
        }
    }
    void startPool(int size)
    {
        for (int i = 0; i < size; i ++)
        {
            _pool.push_back(
                    new Thread( bind(&ThreadPool::runInThread, this, placeholders::_1), i )
                    );
        }
        for (int i = 0; i < size; i ++)
        {
            // _handler获得线程句柄,方便之后可以对线程进行join()回收线程资源
            _handler.push_back(_pool[i]->start());
        }
    }
private:
    vector<Thread*> _pool;
    vector<thread> _handler;


    // runInThread成员函数充当线程函数,即线程池创建的线程就是要运行runInThread这个函数
    // 注意:因为线程函数只能是C函数,而成员函数中有对象的this指针,所以需要通过绑定器过滤一下
    void runInThread(int id)
    {
        cout << "++++++++++++++++" << endl;
        cout << "call runInThread, id: " << id  << endl;
        cout << "++++++++++++++++" << endl;
    }
};

int main()
{
    ThreadPool threadPool;
    threadPool.startPool(10);

    return 0;
}

lambda表达式的使用和原理

传统的函数对象在每一次使用的时候,都需要定义一个类,然后重载operator()之后才能被使用。这样的形式太过复杂。而lambda表达式以短小精炼著称,它可以使用很短的代码做到定义函数对象。

class Lambda
{
public:
    void operator()()
    {
        cout << "hello world" << endl;
    }
};

int main()
{
    // 传统定义函数对象
    Lambda()();
    // lambda表达式定义函数对象
    auto func = []() {cout << "hello world" << endl;};
    func();
    return 0;
}

lambda语法:[捕捉列表](参数列表)->返回值类型 { 函数体 }
其中捕捉列表就是传统定义函数对象的构造函数中参入的参数。可以使用值传参,也可以使用引用传参,对应的构造函数可以用值传递,也可以使用引用传递。
类似的,lambda中的参数列表和函数体对应的就是传统函数对象中operator()中传递的参数列表和函数体。

lambda的应用场景

场景一:map + function + lambda自定义字符串和lambda的映射

就是前面写过的leetcode 150题。

150. 逆波兰表达式求值

有效的算符包括 +、-、*、/ 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
**注意 **两个整数之间的除法只保留整数部分。
可以保证给定的逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。

class Solution {
public:
    typedef long long LL;
    int evalRPN(vector<string>& tokens) {
        unordered_map<string, function<int(int, int)>> ops = {
            {"+", [](LL a, LL b){return a + b;}},
            {"-", [](LL a, LL b){return a - b;}},
            {"*", [](LL a, LL b){return a * b;}},
            {"/", [](LL a, LL b){return a / b;}}
        };
        stack<LL> sk;
        for (string& token: tokens) {
            if (ops.count(token)) {
                int b = sk.top(); sk.pop();
                int a = sk.top(); sk.pop();
                sk.push(ops[token](a, b));
            } else {
                sk.push(stoi(token));
            }
        }
        return sk.top();
    }
};

场景二:function + lambda自定义智能指针删除器

int main()
{
    /**
     * 使用智能指针托管FILE*,最后处理的方式是delete掉这个指针。但是FILE*最后需要被fclose()关闭掉,而不是delete
     * unique_ptr<FILE> ptr(fopen("text.txt", "w"))最后不能关闭文件流
     * 所以这里就需要自定义删除器,而专门为了定制删除器写一个类就太麻烦了,所以可以使用lambda表达式定制删除器
     */
     // function+lambda定制删除器
    unique_ptr<FILE, function<void(FILE*)>> ptr(
            fopen("test.txt", "w"),
            [](FILE* fptr){ fclose(fptr);});
    return 0;
}

场景三:function/decltype + lambda + priority_queue自定义优先队列比较器

typedef pair<int, int> PII;

int main()
{
    // 按照第二个pair<int, int>的第二个关键字以降序排序
    auto cmp = [](PII& a, PII& b) { return a.second < b.second; };

    priority_queue<PII, vector<PII>, function<bool(PII &, PII &)>> heap1(cmp); // 可以将比较器通过构造函数传入类中
    // 使用decltype()可以自动推导出变量的类型
    priority_queue<PII, vector<PII>, decltype(cmp)> heap2(cmp);

    for (int i = 0; i < 10; i ++)
    {
        // 第一个关键字统一给1,第二个关键字放入0~9
        heap1.push({1, i});
    }
    while (heap1.size())
    {
        cout << heap1.top().first << ' ' << heap1.top().second << endl;
        heap1.pop();
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hyzhang_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值