【C++】C++11——函数对象、function函数包装器及其相关工具

本文详细介绍了C++中的函数对象概念,包括自定义和内置的运算符与搜索器,以及std::function的使用和std::bind的高级应用。涵盖了如何包装函数、成员函数和Lambda表达式,以及在实际项目中的线程池应用实例。

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

ScutRobotlab

【C++】函数对象以及常见的std::function包装器

引言

本文介绍了函数对象,以及在诞生于 C++11 新标准下的函数包装器及其涉及到的部分函数应用。大多数介绍的内容均可在 cppreference 上找得到,感兴趣的小伙伴也可以直接跳转至参考文档的第 3 条内容。本文对这些常用的工具做了个汇总,并且展示了在实战代码中是如何用到这些内容的。

1 函数对象

凡是重载 operator() 运算符的对象都属于函数对象。

1.1 自定义函数对象

一般自定义函数对象需要在类中重载函数调用运算符 operator(),示例代码如下:

struct MyPlus
{
    int operator()(int a, int b)
    {
        return a + b;
    }
};

首先需要根据自定义类实例化一个对象,也可以是匿名对象(临时变量),在 main 函数中调用

int main(int argc, char *argv[])
{
    MyPlus plus;
    //          1 + 2                    3 + 4
    cout << plus(1, 2) << ' ' << MyPlus()(3, 4) << endl;
    return 0;
}

运行结果如下:

3 7

1.2 常见内置函数对象

1.2.1 运算符函数对象

这些函数对象在使用前需要包含相应的头文件 #include <functional> 1

常见的算数运算符函数对象有std::plus(+)、std::multiplies(*)……

常见的逻辑运算符函数对象有std::equal_to(==)、std::greater(>)……

std::plus<float> p;
std::multiplies<float> mu;
//             5 + 2             5 * 2
std::cout << p(5, 2) << ' ' << mu(5, 2) << std::endl;

std::equal_to<float> e;
std::greater<uint8_t> g;
//             3.1 == 3.2                16 > 2
std::cout << e(3.1f, 3.2f) << ' ' << g(0x10, 0b10) << std::endl;
1.2.2 搜索器

C++17 提供了若干种搜索器,它们是适合用于 std::search2搜索器 重载的类,它将搜索操作委派到 C++17 前标准库的 std::search。这里介绍默认搜索器:std::default_searcher3,参考代码如下:

int main()
{
    // 定义串和待搜索的子串
    string sentence = "You should have gone for the head.";
    string word = "have";
    auto word_it = default_searcher(word.begin(), word.end());
    
    // 直接使用 std::default_searcher
    auto res = word_it(sentence.begin(), sentence.end());
    cout << '\"' << string(sentence.begin(), res.first) << '\"' << endl;
    
    // 传统 std::search
    auto it_1 = search(sentence.begin(), sentence.end(), word.begin(), word.end());
    cout << '\"' << string(sentence.begin(), it_1) << '\"' << endl;
    
    // std::default_searcher 配合 std::search 使用
    auto it_2 = search(sentence.begin(), sentence.end(), word_it);
    auto it_3 = search(sentence.begin(), sentence.end(),
                       default_searcher(word.begin(), word.end()));
    cout << '\"' << string(sentence.begin(), it_2) << '\"' << endl;
    cout << '\"' << string(sentence.begin(), it_3) << '\"' << endl;
}

it_3 一行创建的是一个匿名的临时对象,这也是配合 std::search 使用时最标准的使用方法。运行结果如下:

"You should "
"You should "
"You should "
"You should "

2 函数包装器与部分函数应用

2.1 函数包装器——std::function

C++11提供了一个通用多态函数包装器std::function<>,使其能够存储、复制、调用任何可复制构造的可调用对象1,包括全局函数、成员函数、函数对象、lambda表达式、bind表达式。以下示例代码为std::function<>对前4种可调用对象进行了包装。

void foo(int a, double b, string c)
{
    cout << a << ' ' << b << ' ' << c << endl;
}

class MyFoo
{
    int _a;

public:
    /* explicit */ MyFoo(int a) : _a(a) {}
    void myFoo(string b) const { cout << _a << ' ' << b << endl; }
};

struct MyFoo2
{
    inline void operator()(float a) { cout << a << endl; }
};

int main()
{
    cout << "<<<<<<<<<<< 全局函数 >>>>>>>>>>>" << endl;
    function<void(int, double, string)> f1 = foo;
    f1(1, 3.14, "abc");
    cout << "<<<<<<<<<<< 成员函数 >>>>>>>>>>>" << endl;
    function<void(const MyFoo &, string)> f2 = &MyFoo::myFoo;
    MyFoo my_foo(123);
    f2(my_foo, "hello");
    f2(666, "hello"); // 调用隐式转换构造,注意不要使用 explicit 关键字
    cout << "<<<<<<<<<<< 函数对象 >>>>>>>>>>>" << endl;
    function<void(float)> f3 = MyFoo2();
    f3(0.123f);
    cout << "<<<<<<<<< lambda表达式 >>>>>>>>>" << endl;
    function<int(int)> f4 = [](int a) -> int { return 2 * a; };
    cout << f4(147) << endl;

    return 0;
}

运行结果如下:

<<<<<<<<<<< 全局函数 >>>>>>>>>>>
1 3.14 abc
<<<<<<<<<<< 成员函数 >>>>>>>>>>>
123 hello
666 hello
<<<<<<<<<<< 函数对象 >>>>>>>>>>>
0.123
<<<<<<<<< lambda表达式 >>>>>>>>>
294

注意:

  1. 此处若使用auto关键字进行自动类型推导,会被当做普通的函数指针,但不影响结果。不过值得注意的是,被推导的函数不得有重载版本,否则编译不通过
    no_auto

  2. 使用包装器对成员函数进行操作时,第一个参数需要传入调用该成员函数的对象,因为成员函数和成员变量的内存是分开存储的(这等效于一个结构体struct和若干函数的内存表示)。而平常在访问成员函数时,编译器会自动将所调用对象地址赋值给 this 指针,因此这里需要指定调用的对象。

2.2 std::bind

2.2.1 bind原型与基本用法

函数原型:此处传参使用变参模板4支持任意个数、任意类型,使用转发引用获取实参

template<typename F, typename... Args>
/*unspecified*/ bind(F&& f, Args&&... args);

函数模板 std::bind 生成 f 的转发调用包装器5。调用此包装器等价于以一些绑定到 args 的参数调用 f,示例代码如下:

void foo(int a, double b, string c)
{
    cout << a << ' ' << b << ' ' << c << endl;
}

int main()
{
    auto f = bind(foo, 123, 3.14, "hello");
    f(); // 运行结果为:123 3.14 hello
    return 0;
}
2.2.2 bind与占位符

除此之外,我们也可以使用_1, _2, _3, ...作为args的占位参数,这将很大提高了bind使用的灵活性,这些占位参数被定义在命名空间std::placeholders5,示例代码如下:

inline void foo(int a, string b, int c, double d)
{
    cout << a << ' ' << b << ' '
         << c << ' ' << d << endl;
}

int main()
{
    function<void(int, string, int, double)> fun = foo;
    auto f = bind(fun, _1, "hello", _1, _2);
    f(3, 2, 4);
    return 0;
}

运行结果如下:

3 hello 3 2

实际上,_1绑定在第一个实参3上,_2绑定在第二个实参2_3绑定在第二个实参4,在调用中,只对_1_2进行了访问,因此形参ac都为3,形参d2

2.2.3 bind用于成员函数与成员变量
  1. 用于成员函数

    bind 的第一个参数永远是可调用对象所在地址,对成员函数进行操作时,使用std::function传参的第一个参数是调用该成员函数的对象,在这种情况下 bind 的第二个参数同样也是调用该成员函数的对象,并且该对象也可使用占位符,示例代码如下:

    struct MyFunction
    {
        void foo(int a, string b, double c, bool d)
        {
            cout << a << b << c << (d ? "True" : "False") << endl;
        }
    };
    
    int main()
    {
        MyFunction fun;
        auto f = bind(&MyFunction::foo, fun, _1, "hello", 3.14, _2);
        f(10, true);
        auto g = bind(&MyFunction::foo, _3, 20, "world", 1.23, false);
        g(100, "my_fun", fun);
        return 0;
    }
    

    在运行 15 行代码时,仅fun被绑定在了_3上,因此前两个参数均无效。运行结果如下:

    10hello3.14True
    20world1.23False
    
  2. 用于成员变量

    前两个参数的调用规则与操作成员函数的一致,由于获取成员变量时无其余操作,因此无需其余参数。示例代码如下:

    struct A
    {
        int num1;
        double num2;
    };
    
    int main()
    {
        A a = {100, 3.14};
        auto f = bind(&A::num1, a);
        cout << f() << endl;
        auto g = bind(&A::num2, _1);
        cout << g(a) << endl; // _1 绑定 a
        return 0;
    }
    

    运行结果如下

    100
    3.14
    

3 实际运用

前不久学了一下线程、进程的相关内容,在开源代码的指导下,摸索着搭了一个线程池库ThreadPool,这里对我遇到过的有关函数包装器的内容做个简单的分享。其中在这个线程池库中有一个添加任务的函数addTask,函数原型如下:

template <typename _Task>
void addTask(_Task &&task);
// 这里使用了转发引用,但任务队列真正需要的 Task 类型定义如下
using Task = std::function<void()>;

该函数要求传入一个无返回值的函数作为任务,若具有通用性,那么这个函数支持任意个数的参数,这点与要求相悖。并且实际上,每个线程在从队列中取用任务时执行的都是相同的操作,如以下代码片段:

// 以下代码为每个线程运行的 loop() 中获取任务以及执行任务的部分
Task task = takeTask(); // 取用任务
if (task)
{
    task(); // 执行任务
}

因此在执行每个task时,需要保证其参数列表一致。

如果每个任务的函数不一样,则可以使用 bind 绑定参数到可调用对象上,最终可以得到一个无参数的包装器,可参考本文 2.2.1 节示例代码。最终作为addTask的参数进行添加任务的步骤,便可成功打入任务队列,可参考以下代码:

// 创建一个 vector
vector<int> mat(100);
// 为线程池添加任务
tp.addTask(bind(
    [](vector<int> &m)
    {
        for (auto &element : m)
            element += 10;
    }, ref(mat)));
// 线程池待所有任务执行完毕后停用
tp.stop(false);

我们一步一步把调用内容拆解开,首先bind的可调用对象为一个lambda表达式,该可调用对象经过std::function包装后可以得到std::function<void(vector<int> &)>类型。

因此bind若要实现零参数,在使用时需传入两个无占位符的参数,第一个就是该lambda表达式,第二个就是传入lambda函数的待处理值:mat,值得注意的是,若在函数包装器中使用引用作为参数,直接传入mat是不行的:在参数传入bind进行绑定时会创建一份新的内存空间副本,实际运行该可调用对象时(此处为lambda函数)才会取该副本的引用,导致真正的值不会被修改。因此需要用到std::ref来得到其左值引用6,若传入的值不需要内部进行修改,则不需要使用std::ref

到这一步,bindref(mat) 绑定在了 lambda 表达式上,其返回值则可以被function<void()>所转换,则可以成功加入任务队列。

参考文档


  1. std::search ↩︎ ↩︎

  2. 默认搜索器 ↩︎

  3. function多态函数包装器 ↩︎

  4. 变参模板 Variadic Templates ↩︎

  5. std::bind 可调用对象参数绑定工具 ↩︎ ↩︎

  6. std::ref ↩︎

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

_Cccolt_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值