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


person Tarod    schedule 05.06.2016    source источник
comment
&, за которым следует идентификатор, не означает ссылку. Нет, если только он не объявляет переменную, и в этом случае & будет предшествовать typename. Когда & применяется к идентификатору, вы получаете указатель.   -  person Nicol Bolas    schedule 05.06.2016
comment
В общем, избегайте использования std::bind. У него есть действительно глубокие причуды, благодаря которым он дает удивительные результаты даже после того, как вы овладеете основами. Я считаю, что оно того не стоит.   -  person Yakk - Adam Nevraumont    schedule 05.06.2016


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

person Dietmar Kühl    schedule 05.06.2016
comment
Прочитав это на полпути, я был озадачен, кто мог написать такое прекрасное объяснение? Я догадался! - person SergeyA; 05.06.2016
comment
Замечательный ответ, Дитмар. Для меня это следующие ключи: (1) Хотя имена функций неявно распадаются на указатели на функции, то же самое не относится к функциям-членам: чтобы получить указатель на функцию-член, необходимо использовать &., (2) std :: bind () обнаруживает, что указатель на функцию-член передается и, скорее всего, превращает его в вызываемый объект, и (3) Чтобы использовать указатель на функцию-член, необходим объект. При использовании указателя на член с помощью std :: bind () второй аргумент std :: bind (), соответственно, должен указывать, откуда исходит объект. Большое тебе спасибо! - person Tarod; 05.06.2016

Из 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 дает полную информацию о том, что ожидает привязка и как она вызывает сохраненную функцию.

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

auto f = [&](int n) { return foo.print_sum(95, n); }
person Dmitry Panteleev    schedule 05.06.2016
comment
Как ни странно, &Foo::print_sum не вызывается! Указатель на член, даже указатель на функцию-член, не имеет оператора вызова функции! std::mem_fn(&Foo::print_sum) будет вызываемым. Однако std::bind() понимает, что вызывается с указателем на члены в качестве первого аргумента. - person Dietmar Kühl; 05.06.2016
comment
@ DietmarKühl Callable в C ++ 17 означает несколько иное: en.cppreference.com/ w / cpp / types / is_callable. И все семейство std::mem_fn, std::bind и std::invoke просто обеспечивает приятный интерфейс по сравнению с базовыми типами Callable. - person Dmitry Panteleev; 05.06.2016
comment
Но это всего лишь вопрос терминологии. - person Dmitry Panteleev; 05.06.2016
comment
Спасибо @DmitryPanteleev! Я также читал очень хороший ответ здесь, связанный с указателями функций и указателями на функции-члены. - person Tarod; 05.06.2016