web-dev-qa-db-fra.com

Comment std :: bind fonctionne avec les fonctions membres

Je travaille avec std::bind mais je ne comprends toujours pas comment cela fonctionne lorsque nous l'utilisons avec des fonctions de classe membre.

Si nous avons la fonction suivante:

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

Je comprends parfaitement les prochaines lignes de code:

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

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

Mais maintenant, avec le code suivant où nous avons une fonction de liaison à un membre, j'ai quelques questions.

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);
  • Pourquoi le premier argument est-il une référence? J'aimerais avoir une explication théorique.

  • Le deuxième argument est une référence à l'objet et c'est pour moi la partie la plus compliquée à comprendre. Je pense que c'est parce que std::bind a besoin d'un contexte, ai-je raison? Est-ce toujours comme ça? A std::bind une sorte d'implémentation nécessitant une référence lorsque le premier argument est une fonction membre?

19
Tarod

Lorsque vous dites "le premier argument est une référence", vous vouliez sûrement dire "le premier argument est un pointeur": l'opérateur & Prend l'adresse d'un objet, donnant un pointeur .

Avant de répondre à cette question, prenons un peu de recul et examinons votre première utilisation de std::bind() lorsque vous utilisez

std::bind(my_divide, 2, 2)

vous fournissez une fonction. Lorsqu'une fonction est passée n'importe où, elle se désintègre en un pointeur. L'expression ci-dessus est équivalente à celle-ci, prenant explicitement l'adresse

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

Le premier argument de std::bind() est un objet identifiant comment appeler une fonction. Dans le cas ci-dessus, il s'agit d'un pointeur pour fonctionner avec le type double(*)(double, double). Tout autre objet appelable avec un opérateur d'appel de fonction approprié ferait aussi l'affaire.

Comme les fonctions membres sont assez courantes, std::bind() fournit un support pour traiter le pointeur vers les fonctions membres. Lorsque vous utilisez &print_sum, Vous obtenez simplement un pointeur sur une fonction membre, c'est-à-dire une entité de type void (Foo::*)(int, int). Alors que les noms de fonction se désintègrent implicitement des pointeurs vers des fonctions, c'est-à-dire que le & Peut être omis, il n'en va pas de même pour les fonctions membres (ou les membres de données, d'ailleurs): pour obtenir un pointeur sur une fonction membre, il est nécessaire pour utiliser le &.

Notez qu'un pointeur sur un membre est spécifique à un class mais il peut être utilisé avec n'importe quel objet de cette classe. Autrement dit, il est indépendant de tout objet particulier. C++ n'a pas de moyen direct d'obtenir une fonction membre directement liée à un objet (je pense qu'en C #, vous pouvez obtenir des fonctions directement liées à un objet en utilisant un objet avec un nom de membre appliqué; cependant, cela fait plus de 10 ans que J'ai programmé pour la dernière fois un peu de C #).

En interne, std::bind() détecte qu'un pointeur vers une fonction membre est passé et le transforme très probablement en objets appelables, par exemple, en utilisant std::mem_fn() avec son premier argument. Puisqu'une fonction membre nonstatic a besoin d'un objet, le premier argument de l'objet appelable de résolution est soit une référence, soit un pointeur [intelligent] vers un objet de la classe appropriée.

Pour utiliser un pointeur sur une fonction membre, un objet est nécessaire. Lorsque vous utilisez un pointeur sur un membre avec std::bind(), le deuxième argument de std::bind() doit en conséquence spécifier la provenance de l'objet. Dans votre exemple

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

l'objet appelable résultant utilise &foo, c'est-à-dire un pointeur vers foo (de type Foo*) comme objet. std::bind() est assez intelligent pour utiliser tout ce qui ressemble à un pointeur, tout ce qui peut être converti en une référence du type approprié (comme std::reference_wrapper<Foo>), ou une [copie] d'un objet comme objet lorsque le premier argument est un pointeur sur membre.

Je suppose que vous n'avez jamais vu de pointeur vers un membre - sinon ce serait assez clair. Voici un exemple simple:

#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);
}

La fonction apply() obtient simplement deux pointeurs vers les objets Foo et un pointeur vers une fonction membre. Il appelle la fonction membre pointée avec chacun des objets. Cet opérateur ->* Amusant applique un pointeur sur un membre à un pointeur sur un objet. Il existe également un opérateur .* Qui applique un pointeur sur un membre à un objet (ou, comme ils se comportent comme des objets, une référence à un objet). Puisqu'un pointeur vers une fonction membre a besoin d'un objet, il est nécessaire d'utiliser cet opérateur qui demande un objet. En interne, std::bind() organise la même chose.

Lorsque apply() est appelé avec les deux pointeurs et &Foo::f, Il se comporte exactement comme si le membre f() était appelé sur les objets respectifs. De même, lorsque vous appelez apply() avec les deux pointeurs et &Foo::g, Il se comporte exactement de la même manière que si le membre g() était appelé sur les objets respectifs (le comportement sémantique est le idem mais le compilateur aura probablement beaucoup plus de mal à intégrer des fonctions et échoue généralement à le faire lorsque des pointeurs vers des membres sont impliqués).

34
Dietmar Kühl

De std :: bind docs :

bind( F&& f, Args&&... args ); où f est un Callable, dans votre cas, c'est un pointeur sur la fonction membre. Ce type de pointeurs a une syntaxe particulière par rapport aux pointeurs vers les fonctions habituelles:

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 (Et std::invokeen général ) couvre tous ces cas de manière uniforme. Si f est un pointeur vers un membre de Foo, alors le premier Arg fourni pour se lier devrait être une instance de Foo (bind(&Foo::print_sum, foo, ...) fonctionne aussi, mais foo est copié) ou un pointeur vers Foo, comme dans l'exemple tu avais.

Voici quelques informations supplémentaires sur pointeurs vers les membres , et 1 et 2 donne des informations complètes sur ce que la liaison attend et comment il appelle la fonction stockée.

Vous pouvez également utiliser des lambdas à la place std::bind, Ce qui pourrait être plus clair:

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