Как std:: bind работает с функциями-членами

Я работаю с std::bind, но до сих пор не понимаю, как это работает, когда мы используем его с функциями классов-членов.

Если у нас есть следующая функция:

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 нужен контекст, я прав? Это всегда так? Имеет ли std::bind какую-то реализацию, требующую ссылки, когда первый аргумент является функцией-членом?

Ответ 1

Когда вы говорите: "первый аргумент - это ссылка", вы наверняка хотели сказать "первый аргумент - это указатель": оператор & принимает адрес объекта, уступая указателю.

Прежде чем отвечать на этот вопрос, давайте кратко отступим и посмотрим на ваше первое использование 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, но он может использоваться с любым объектом этого класса. То есть, он не зависит от какого-либо конкретного объекта. С++ не имеет прямого способа получить функцию-член, напрямую связанную с объектом (я думаю, что на С# вы можете получить функции, напрямую связанные с объектом, с использованием объекта с именем прикладного участника, однако это уже 10+ лет Я последний раз запрограммировал немного С#).

Внутри std::bind() обнаруживает, что указатель на функцию-член передан и, скорее всего, превращает его в вызываемые объекты, например, используя std::mem_fn() с его первым аргументом. Поскольку функция-член не static нуждается в объекте, первый аргумент вызываемого объекта разрешения является либо ссылочным, либо [умным] указателем на объект соответствующего класса.

Чтобы использовать указатель на функцию-член, необходим объект. При использовании указателя на член с std::bind() второй аргумент std::bind() соответственно должен указывать, когда объект идет. В вашем примере

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

в результате вызываемого объекта используется &foo, то есть указатель на foo (типа Foo*) в качестве объекта. std::bind() достаточно умен, чтобы использовать что-либо, похожее на указатель, все, что может быть конвертировано в ссылку соответствующего типа (например, std::reference_wrapper<Foo>) или [копировать] объекта в качестве объекта, когда первым аргументом является указатель на член.

Я подозреваю, вы никогда не видели указателя на члена - иначе было бы совершенно ясно. Вот простой пример:

#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 и указатель на функцию-член. Он вызывает функцию-член, указанную с каждым из объектов. Этот забавный оператор ->* применяет указатель на элемент к указателю на объект. Существует также оператор .*, который применяет указатель на элемент к объекту (или, как они ведут себя как объекты, ссылку на объект). Поскольку указатель на функцию-член нуждается в объекте, необходимо использовать этот оператор, который запрашивает объект. Внутри std::bind() организует то же самое.

Когда apply() вызывается с двумя указателями и &Foo::f, он ведет себя точно так же, как если бы член f() вызывался на соответствующие объекты. Аналогично при вызове apply() с двумя указателями и &Foo::g он ведет себя точно так же, как если бы член g() вызывался на соответствующие объекты (семантическое поведение такое же, но у компилятора, вероятно, будет намного сложнее и обычно отказывается делать это, когда задействованы указатели на элементы).

Ответ 2

От 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::bindstd::invoke в целом) охватывает все эти случаи единым способом. Если f является указателем на член Foo, то первый Arg, предоставленный для привязки, как ожидается, будет экземпляром Foo (bind(&Foo::print_sum, foo, ...) также работает, но Foo скопирован) или указатель на Foo, как в примере, который у вас был.

Ниже приведено более подробное описание указателей на элементы, а 1 и 2 дает полную информацию о том, что ожидает связка и как она вызывает сохраненную функцию.

Вы также можете использовать lambdas вместо std::bind, что может быть более ясным:

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