std::bind 如何与成员函数一起工作

How std::bind works with member functions

我正在使用 std::bind,但当我们将它与成员 class 函数一起使用时,我仍然不明白它是如何工作的。

如果我们有以下函数:

double my_divide (double x, double y) {return x/y;}

我完全理解接下来的代码行:

auto fn_half = std::bind (my_divide,_1,2);               // returns x/2

std::cout << fn_half(10) << '\n';                        // 5

但是现在,对于绑定到成员函数的以下代码,我有一些疑问。

struct Foo {
    void print_sum(int n1, int n2)
    {
        std::cout << n1+n2 << '\n';
    }
    int data = 10;
};

Foo foo;

auto f = std::bind(&Foo::print_sum, &foo, 95, _1);
f(5);

来自 std::bind docs:

bind( F&& f, Args&&... args ); 其中 f 是一个 Callable,在您的例子中是指向成员函数的指针。与普通函数的指针相比,这种指针有一些特殊的语法:

typedef  void (Foo::*FooMemberPtr)(int, int);

// obtain the pointer to a member function
FooMemberPtr a = &Foo::print_sum; //instead of just a = my_divide

// use it
(foo.*a)(1, 2) //instead of a(1, 2)

std::bind(和std::invoke in general)以统一的方式涵盖了所有这些情况。如果 f 是指向 Foo 成员的指针,那么提供给绑定的第一个 Arg 应该是 Foo 的一个实例(bind(&Foo::print_sum, foo, ...) 也是有效,但 foo 被复制)或指向 Foo 指针 ,就像您的示例一样。

这里有一些关于 pointers to members, and 1 and 2 的更多阅读,提供了有关绑定期望的内容以及它如何调用存储函数的完整信息。

您也可以使用 lambda 代替 std::bind,这样会更清楚:

auto f = [&](int n) { return foo.print_sum(95, n); }

当你说 "the first argument is a reference" 时,你肯定是想说 "the first argument is a pointer":& 运算符获取对象的地址,产生一个指针。

在回答这个问题之前,让我们先简单回顾一下你第一次使用std::bind()时使用

std::bind(my_divide, 2, 2)

你提供了一个功能。当一个函数被传递到任何地方时,它会退化成一个指针。上面的表达式等价于这个,显式取地址

std::bind(&my_divide, 2, 2)

std::bind() 的第一个参数是一个标识如何调用函数的对象。在上面的例子中,它是一个指向类型为 double(*)(double, double) 的函数的指针。任何其他具有合适函数调用运算符的可调用对象也可以。

由于成员函数很常见,std::bind() 提供了对处理指向成员函数的指针的支持。当你使用 &print_sum 时,你只是得到一个指向成员函数的指针,即 void (Foo::*)(int, int) 类型的实体。虽然函数名称隐式衰减为指向函数的指针,即 & 可以省略,但对于成员函数(或数据成员,就此而言)并非如此:要获取指向成员函数的指针,它是必须使用 &.

请注意,指向成员的指针特定于 class,但它可以用于 class 的任何对象。也就是说,它独立于任何特定对象。 C++ 没有直接获取直接绑定到对象的成员函数的方法(我认为在 C# 中,您可以通过使用具有应用成员名称的对象来获取直接绑定到对象的函数;但是,自从 10 多年前我最后编写了一些 C#)。

在内部,std::bind() 检测到传递了指向成员函数的指针,并且很可能将其转换为可调用对象,例如,通过使用 std::mem_fn() 及其第一个参数。由于非 static 成员函数需要一个对象,因此解析可调用对象的第一个参数是引用或指向适当 class.

对象的 [智能] 指针

要使用指向成员函数的指针,需要一个对象。当使用指向带有 std::bind() 的成员的指针时,std::bind() 的第二个参数相应地需要指定对象的来源。在你的例子中

std::bind(&Foo::print_sum, &foo, 95, _1)

生成的可调用对象使用 &foo,即指向 fooFoo* 类型)的指针作为对象。 std::bind() 足够聪明,可以使用任何看起来像指针的东西,任何可转换为适当类型的引用的东西(如 std::reference_wrapper<Foo>),或者一个对象的 [copy] 作为第一个参数时的对象是指向成员的指针。

我怀疑,您从未见过指向成员的指针 - 否则它会很清楚。这是一个简单的例子:

#include <iostream>

struct Foo {
    int value;
    void f() { std::cout << "f(" << this->value << ")\n"; }
    void g() { std::cout << "g(" << this->value << ")\n"; }
};

void apply(Foo* foo1, Foo* foo2, void (Foo::*fun)()) {
    (foo1->*fun)();  // call fun on the object foo1
    (foo2->*fun)();  // call fun on the object foo2
}

int main() {
    Foo foo1{1};
    Foo foo2{2};

    apply(&foo1, &foo2, &Foo::f);
    apply(&foo1, &foo2, &Foo::g);
}

函数apply()只是得到两个指向Foo对象的指针和一个指向成员函数的指针。它调用每个对象指向的成员函数。这个有趣的 ->* 运算符将指向成员的指针应用到指向对象的指针。还有一个 .* 运算符,它将指向成员的指针应用到对象(或者,因为它们的行为就像对象一样,是对对象的引用)。由于指向成员函数的指针需要一个对象,因此有必要使用请求对象的 this 运算符。在内部,std::bind() 安排了同样的事情发生。

当使用两个指针和 &Foo::f 调用 apply() 时,它的行为与成员 f() 将在各自的对象上调用完全相同。同样,当用两个指针调用 apply()&Foo::g 时,它的行为与成员 g() 将在各自的对象上调用完全相同(语义行为相同,但编译器是内联函数可能会更加困难,并且在涉及指向成员的指针时通常会失败。