我是靠谱客的博主 舒心咖啡,最近开发中收集的这篇文章主要介绍绑定器和函数对象的实现原理,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

  • 目录
    • 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;
}

最后

以上就是舒心咖啡为你收集整理的绑定器和函数对象的实现原理的全部内容,希望文章能够帮你解决绑定器和函数对象的实现原理所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(43)

评论列表共有 0 条评论

立即
投稿
返回
顶部