Skip to content

std::function与可调用实体

约 1575 字大约 5 分钟

C++

2024-11-11

C++中的可调用实体

在C++中,可调用实体(Callable Entity)是指可以像函数一样被调用的对象。主要包括以下几类:

  1. 函数指针、普通函数
  2. 函数对象(仿函数)
  3. lambda表达式
  4. 静态成员函数、非静态成员函数
普通函数
int add(int a = 1, int b = 2) {
    return a + b;
}

int main() {
    add();
    return 0;
}

上述的例子中,可调用实体和调用函数的形式是一样的(都是用()调用)。

本质上讲,可调用实体在编译时他们的执行逻辑写入了代码段(.text段)。而C++作为一个强类型的语言,不同的可调用实体都被视为是不一样的类型。

std::function

C++11提供了一种可调用实体包装器std::function,他是一个类模板,可以容纳所有的可调用实体。

#include <functional>

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

auto add2 = [](int a, int b) { return a + b; };

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

class Add2 {
public:
  static int add4(int a, int b) { return a + b; }
  int add5(int a, int b) { return a + b; }
};

int main() {

  std::function<int(int, int)> func1 = add1;
  std::function<int(int, int)> func2 = add2;
  std::function<int(int, int)> func3 = add3;
  std::function<int(int, int)> func4 = &Add2::add4;

  Add2 a;
  // 成员函数隐含this指针,必须使用std::bind处理
  std::function<int(int, int)> func5 =
      std::bind(&Add2::add5, &a, std::placeholders::_1, std::placeholders::_2);
  return 0;
}

将可调用实体赋给std::function时,注意,是否需要使用&符号来取地址,每个可调用实体的表现是不一样的。

  • 普通函数:为了兼容C,现代C++可以认为函数名和对函数进行取地址是等价的,函数名可以认为就是函数的入口地址。
  • lambda表达式:本质上是一个对象,而std::function可以直接接收可调用实例对象,不需要&
  • 仿函数:仿函数是一个类,类中重载了()操作符,所以仿函数名本质是类名,而不是函数名。所以实例对象不需要&
  • 静态成员函数:需要使用&取函数入口地址。
  • 非静态成员函数:需要使用std::bind显式传入对象的地址

std::bind

C++11标准库提供了一个std::bind函数,可以用来将可调用实体和参数绑定在一起,生成一个新的可调用实体。

std::placeholders占位符

参数可以使用std::placeholders占位符,表示在调用时再指定具体的值。也可以直接给定具体的参数,类似于绑定了默认值。

可以说std::bind提供了一种改变可调用实体参数位置以及默认值的方式。

#include <iostream>
void show(int a, int b) {
    std::cout << a << " " << b << std::endl;
}

int main() {
    auto func = std::bind(show, std::placeholders::_1, std::placeholders::_2);
    func(1, 2);
    return 0;
}

通常的使用形式:std::bind(<可调用实体>, <参数1, 参数2, ...>),这里第一个参数是可调用实体,后面的参数是可调用实体的参数。

后面的参数与可调用实体的参数是一一对应的关系。即传给std::bind的参数,会按顺序传给可调用实体。

占位符的作用:绑定调用新的可调用对象时对应位置的参数。

#include <iostream>
void show(int a, int b, int c) {
    std::cout << a << " " << b << " " << c << std::endl;
}

int main() {
    auto func1 = std::bind(show, 1, 2, 3);

    // 1,2和3会按顺序传给show,所以此处可以不写参数
    // 需要注意的是,即便写参数也无效,还是会传入std::bind绑定的值。
    func1();
    // 输出:1 2 3

    // 占位符的作用:不传入默认值,指定在调用新的可调用对象时再传入。
    auto func2 = std::bind(show, std::placeholders::_1, std::placeholders::_2, 3);
    // 这里的1就传给了_1, 2传给了_2
    func2(1, 2);
    // 输出:1 2 3

    auto func3 = std::bind(show, std::placeholders::_1, 2, std::placeholders::_2)
    // 这里的1传给了_1, 2传给了2,3没有意义,甚至可以不写,
    // func3需要多少个参数取决于占位符的最大值
    func3(1, 2, 3);
    // 输出:1 2 2
    return 0;
}

再重新梳理以下占位符的意义:

对于show函数而言,其本身被调用必然需要传入三个参数,这点是毋庸置疑的。而std::bind以及std::placeholders的作用不过是改变参数顺序或者指定默认值而已。

auto func = std::bind(show, <1>, <2>, <3>)这里的<1>, <2>, <3>会作为参数会按顺序传给show函数。

如果在<1>, <2>, <3>中使用了std::placeholders,他指代的是func上对应位置的参数。

例如:func(3, 4, 5),那么std::placeholders::_1指代的就是3,std::placeholders::_2指代的就是4,std::placeholders::_3指代的就是5。

所以,std::bind返回的可调用对象func中的参数个数,取决于std::bind占位符所使用的最大值,因为占位符决定了在func的参数列表的对应位置的参数需要被使用,而其他未提及的参数则没有意义。

非静态成员函数使用std::bind

非静态成员函数与实例对象是绑定的,并且非静态成员实际上在第一个参数位置上隐含this指针。所以,使用std::bind绑定非静态成员函数时,需要显式传入实例对象的地址。

#include <iostream>
struct A {
    void show(int a, int b) { std::cout << a << " " << b << std::endl; }
}
int main() {
    A a;
    // &a 传入实例对象的地址
    auto func = std::bind(&A::show, &a, std::placeholders::_1, std::placeholders::_2);
    func(1, 2);
    return 0;
}

使用场景

在形参中使用std::function作为可调用实体的包装器,这样就可以将可调用实体作为参数传递给函数。这种场景在回调机制中经常被用到。