Utilisation des concepts C++20 pour éviter std :: function

Aug 22 2020

Dans le passé, lorsque je voulais un rappel comme paramètre de fonction, je décidais généralement d'utiliser std::function. Dans de rares cas où je n'utilise jamais de captures, j'ai utilisé une typedefdéclaration de fonction à la place.

Ainsi, généralement ma déclaration avec un paramètre de rappel ressemble à ceci :

struct Socket
{
  void on_receive(std::function<void(uint8_t*, unsigned long)> cb);
}

Cependant, pour autant que je sache, il std::functionfait en fait un peu de travail au moment de l'exécution car il doit résoudre le lambda avec ses captures dans le std::functionmodèle et déplacer/copier ses captures (?).

En lisant les nouvelles fonctionnalités de C++ 20, j'ai pensé que je pourrais utiliser des concepts pour éviter d'utiliser std::functionet utiliser un paramètre contraint pour tout foncteur viable.

Et c'est là que mon problème survient : puisque je veux travailler avec les objets du foncteur de rappel dans le futur, je dois les stocker. Étant donné que je n'ai pas de type définitif pour mon rappel, ma pensée initiale était de copier (éventuellement déplacer à un moment donné) le foncteur à entasser et d'utiliser un std::vector<void*>pour noter où je les ai laissés.

template<typename Functor>
concept ReceiveCallback = std::is_invocable_v<Functor, uint8_t*, unsigned long>
                       && std::is_same_v<typename std::invoke_result<Functor, uint8_t*, unsigned long>::type, void>
                       && std::is_copy_constructible_v<Functor>;
struct Socket
{
  std::vector<void*> callbacks;

  template<ReceiveCallback TCallback>
  void on_receive(TCallback const& callback)
  {
    callbacks.push_back(new TCallback(callback));
  }
}

int main(int argc, char** argv)
{
  Socket* sock;
  // [...] inialize socket somehow

  sock->on_receive([](uint8_t* data, unsigned long length)
                   {
                     // NOP for now
                   });

  // [...]
}

Bien que cela fonctionne assez bien, lors de l'implémentation de la méthode censée appeler le foncteur, j'ai remarqué que je viens de reporter le problème du type inconnu/manquant. Pour autant que je sache, lancer un void*pointeur de fonction ou un hack similaire devrait donner UB - Comment le compilateur saurait-il que j'essaie en fait d'appeler operator() d'une classe complètement inconnue?

J'ai pensé à stocker le foncteur (copié) avec le pointeur de fonction sur sa operator()définition, mais je n'ai aucune idée de comment je pourrais injecter le foncteur thisà l'intérieur de la fonction, et sans cela, je doute que les captures fonctionnent.

Une autre approche que j'avais consistait à déclarer une interface virtuelle pure qui déclare la operator()fonction requise. Malheureusement, mon compilateur m'interdit de convertir mon foncteur en interface et je ne pense pas non plus qu'il existe un moyen légal de laisser le lambda en dériver.

Alors, y a-t-il un moyen de résoudre ce problème ou est-ce que j'utilise peut-être à mauvais escient la fonctionnalité d'exigences/concepts de modèle ?

Réponses

13 NicolBolas Aug 22 2020 at 00:35

Votre version initiale utilisée std::functionprécisément parce qu'elle efface le type. Si vous voulez l'effacement de type (et vous le faites clairement, puisque vous voulez que l'utilisateur puisse utiliser n'importe quel type sans que votre code sache explicitement ce qu'est ce type), alors vous avez besoin d'une forme d'effacement de type. Et l'effacement de texte n'est pas gratuit.

Les contraintes concernent les modèles. Vous ne voulez pas de fonction de modèle ; vous voulez une seule fonction qui traite un appelable de type effacé.

Et pour les rappels qui doivent survivre à la pile d'appels du fournisseur, std::functionla surcharge de est à peu près ce dont vous avez besoin. C'est-à-dire que le "overhead" n'est pas inutile ; c'est ce qui vous permet de stocker des objets de types arbitraires et inconnus dans votre processeur de rappel.