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

2 ответа

Решение

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

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

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

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

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

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

auto f = [&](int n) { return foo.print_sum(95, n); }
Другие вопросы по тегам