Fonction passée comme argument de modèle

225

Je cherche les règles impliquant le passage des fonctions de modèles C ++ comme arguments.

Ceci est pris en charge par C ++ comme le montre un exemple ici:

#include <iostream>

void add1(int &v)
{
  v+=1;
}

void add2(int &v)
{
  v+=2;
}

template <void (*T)(int &)>
void doOperation()
{
  int temp=0;
  T(temp);
  std::cout << "Result is " << temp << std::endl;
}

int main()
{
  doOperation<add1>();
  doOperation<add2>();
}

Il est cependant difficile d'apprendre à connaître cette technique. La recherche de "fonction comme argument de modèle" ne mène pas à grand-chose. Et les modèles C ++ classiques Le guide complet ne l'étonnent pas non plus (du moins pas de ma recherche).

Les questions que je me pose sont de savoir s'il s'agit d'un C ++ valide (ou simplement d'une extension largement prise en charge).

De plus, existe-t-il un moyen de permettre à un foncteur avec la même signature d'être utilisé de manière interchangeable avec des fonctions explicites pendant ce type d'invocation de modèle?

Ce qui suit ne fonctionne pas dans le programme ci-dessus, au moins dans Visual C ++ , car la syntaxe est évidemment incorrecte. Ce serait bien de pouvoir désactiver une fonction pour un foncteur et vice versa, de la même manière que vous pouvez passer un pointeur de fonction ou un foncteur à l'algorithme std :: sort si vous souhaitez définir une opération de comparaison personnalisée.

   struct add3 {
      void operator() (int &v) {v+=3;}
   };
...

    doOperation<add3>();

Des pointeurs vers un ou deux liens Web ou une page du livre des modèles C ++ seraient appréciés!

SPWorley
la source
Quel est l'avantage d'une fonction comme argument de modèle? Le type de retour ne serait-il pas utilisé comme type de modèle?
DaClown
En relation: un lambda sans capture peut se désintégrer en un pointeur de fonction, et vous pouvez le passer en tant que paramètre de modèle en C ++ 17. Clang le compile bien, mais le gcc (8.2) actuel a un bogue et le rejette incorrectement comme n'ayant "aucun lien" même avec -std=gnu++17. Puis-je utiliser le résultat d'un opérateur de conversion lambda constexpr sans capture C ++ 17 comme argument de non-type de modèle de pointeur de fonction? .
Peter Cordes

Réponses:

124

Oui, c'est valable.

Quant à le faire fonctionner avec des foncteurs également, la solution habituelle est quelque chose comme ceci à la place:

template <typename F>
void doOperation(F f)
{
  int temp=0;
  f(temp);
  std::cout << "Result is " << temp << std::endl;
}

qui peut maintenant être appelé soit:

doOperation(add2);
doOperation(add3());

Voir en direct

Le problème avec cela est que si cela rend difficile pour le compilateur d'appeler l'appel add2, car tout ce que le compilateur sait, c'est qu'un type de pointeur de fonction void (*)(int &)est passé à doOperation. (Mais add3, étant un foncteur, il peut être facilement inséré. Ici, le compilateur sait qu'un objet de type add3est passé à la fonction, ce qui signifie que la fonction à appeler est add3::operator(), et pas seulement un pointeur de fonction inconnu.)

jalf
la source
19
Voici maintenant une question intéressante. Lorsque vous passez un nom de fonction, ce n'est PAS comme si un pointeur de fonction était impliqué. C'est une fonction explicite, donnée au moment de la compilation. Le compilateur sait donc exactement ce qu'il a au moment de la compilation.
SPWorley
1
Il y a un avantage à utiliser des foncteurs par rapport aux pointeurs de fonction. Le foncteur peut être instancié à l'intérieur de la classe et fournit ainsi plus d'opertunité au compilateur pour les optimisations (comme l'inline). Le compilateur aurait du mal à optimiser un appel sur un pointeur de fonction.
Martin York
11
Lorsque la fonction est utilisée dans un paramètre de modèle, elle «se désintègre» en un pointeur vers la fonction passée. Il est analogue à la façon dont les tableaux se désintègrent en pointeurs lorsqu'ils sont passés comme arguments aux paramètres. Bien sûr, la valeur du pointeur est connue au moment de la compilation et doit pointer vers une fonction avec une liaison externe afin que le compilateur puisse utiliser ces informations à des fins d'optimisation.
CB Bailey
5
Avance rapide jusqu'à quelques années plus tard, la situation avec l'utilisation de fonctions comme arguments de modèle s'est beaucoup améliorée en C ++ 11. Vous n'êtes plus obligé d'utiliser Javaisms comme les classes functor et pouvez utiliser par exemple des fonctions statiques en ligne comme arguments de modèle directement. Bien loin de la comparaison avec les macros Lisp des années 1970, mais C ++ 11 a certainement de bons progrès au fil des ans.
pfalcon
5
puisque c ++ 11 ne vaudrait-il pas mieux prendre la fonction comme référence rvalue ( template <typename F> void doOperation(F&& f) {/**/}), donc bind par exemple peut passer une expression bind au lieu de la lier?
user1810087
70

Les paramètres du modèle peuvent être paramétrés par type (nom de type T) ou par valeur (int X).

La manière «traditionnelle» C ++ de modéliser un morceau de code consiste à utiliser un foncteur - c'est-à-dire que le code se trouve dans un objet et que l'objet donne ainsi au code un type unique.

Lorsque vous travaillez avec des fonctions traditionnelles, cette technique ne fonctionne pas bien, car un changement de type n'indique pas une fonction spécifique - elle spécifie plutôt uniquement la signature de nombreuses fonctions possibles. Alors:

template<typename OP>
int do_op(int a, int b, OP op)
{
  return op(a,b);
}
int add(int a, int b) { return a + b; }
...

int c = do_op(4,5,add);

N'est pas équivalent au cas du foncteur. Dans cet exemple, do_op est instancié pour tous les pointeurs de fonction dont la signature est int X (int, int). Le compilateur devrait être assez agressif pour aligner entièrement ce cas. (Je ne l'exclurais pas cependant, car l'optimisation du compilateur est devenue assez avancée.)

Une façon de dire que ce code ne fait pas tout à fait ce que nous voulons est:

int (* func_ptr)(int, int) = add;
int c = do_op(4,5,func_ptr);

est toujours légal, et il est clair que cela ne s'aligne pas. Pour obtenir une intégration complète, nous devons créer un modèle par valeur, de sorte que la fonction est entièrement disponible dans le modèle.

typedef int(*binary_int_op)(int, int); // signature for all valid template params
template<binary_int_op op>
int do_op(int a, int b)
{
 return op(a,b);
}
int add(int a, int b) { return a + b; }
...
int c = do_op<add>(4,5);

Dans ce cas, chaque version instanciée de do_op est instanciée avec une fonction spécifique déjà disponible. Ainsi, nous nous attendons à ce que le code de do_op ressemble beaucoup à "return a + b". (Programmeurs Lisp, arrêtez votre sourire!)

Nous pouvons également confirmer que cela est plus proche de ce que nous voulons parce que:

int (* func_ptr)(int,int) = add;
int c = do_op<func_ptr>(4,5);

échouera à la compilation. GCC dit: "erreur: 'func_ptr' ne peut pas apparaître dans une expression constante. En d'autres termes, je ne peux pas développer complètement do_op parce que vous ne m'avez pas donné suffisamment d'informations au moment du compilateur pour savoir ce qu'est notre op.

Donc, si le deuxième exemple est vraiment en train d'intégrer pleinement notre op, et le premier ne l'est pas, à quoi sert le modèle? Qu'est-ce que ça fait? La réponse est: saisissez la contrainte. Ce riff sur le premier exemple fonctionnera:

template<typename OP>
int do_op(int a, int b, OP op) { return op(a,b); }
float fadd(float a, float b) { return a+b; }
...
int c = do_op(4,5,fadd);

Cet exemple fonctionnera! (Je ne dis pas que c'est du bon C ++ mais ...) Ce qui s'est passé, c'est que do_op a été calqué sur les signatures des différentes fonctions, et chaque instanciation séparée écrira un code de coercition de type différent. Ainsi, le code instancié pour do_op avec fadd ressemble à quelque chose comme:

convert a and b from int to float.
call the function ptr op with float a and float b.
convert the result back to int and return it.

Par comparaison, notre cas par valeur nécessite une correspondance exacte sur les arguments de la fonction.

Ben Supnik
la source
2
Voir stackoverflow.com/questions/13674935/… pour une question de suivi en réponse directe à l'observation ici qui int c = do_op(4,5,func_ptr);"n'est clairement pas alignée".
Dan Nissenbaum
Voir ici pour un exemple de cela étant en ligne: stackoverflow.com/questions/4860762/… Il semble que les compilateurs deviennent assez intelligents de nos jours.
BigSandwich
15

Les pointeurs de fonction peuvent être passés en tant que paramètres de modèle, et cela fait partie du C ++ standard . Cependant, dans le modèle, ils sont déclarés et utilisés en tant que fonctions plutôt que pointeur vers fonction. À l' instanciation du modèle on passe l'adresse de la fonction plutôt que juste le nom.

Par exemple:

int i;


void add1(int& i) { i += 1; }

template<void op(int&)>
void do_op_fn_ptr_tpl(int& i) { op(i); }

i = 0;
do_op_fn_ptr_tpl<&add1>(i);

Si vous souhaitez passer un type de foncteur comme argument de modèle:

struct add2_t {
  void operator()(int& i) { i += 2; }
};

template<typename op>
void do_op_fntr_tpl(int& i) {
  op o;
  o(i);
}

i = 0;
do_op_fntr_tpl<add2_t>(i);

Plusieurs réponses passent une instance de foncteur en argument:

template<typename op>
void do_op_fntr_arg(int& i, op o) { o(i); }

i = 0;
add2_t add2;

// This has the advantage of looking identical whether 
// you pass a functor or a free function:
do_op_fntr_arg(i, add1);
do_op_fntr_arg(i, add2);

Le plus proche de cette apparence uniforme avec un argument de modèle est de définir do_opdeux fois - une fois avec un paramètre non type et une fois avec un paramètre type.

// non-type (function pointer) template parameter
template<void op(int&)>
void do_op(int& i) { op(i); }

// type (functor class) template parameter
template<typename op>
void do_op(int& i) {
  op o; 
  o(i); 
}

i = 0;
do_op<&add1>(i); // still need address-of operator in the function pointer case.
do_op<add2_t>(i);

Honnêtement, je m'attendais vraiment à ce que cela ne compile pas, mais cela a fonctionné pour moi avec gcc-4.8 et Visual Studio 2013.

Kietz
la source
9

Dans votre modèle

template <void (*T)(int &)>
void doOperation()

Le paramètre Test un paramètre de modèle non type. Cela signifie que le comportement de la fonction de modèle change avec la valeur du paramètre (qui doit être fixée au moment de la compilation, quelles sont les constantes de pointeur de fonction).

Si vous voulez quelque chose qui fonctionne avec les objets de fonction et les paramètres de fonction, vous avez besoin d'un modèle typé. Cependant, lorsque vous effectuez cette opération, vous devez également fournir une instance d'objet (soit une instance d'objet de fonction ou un pointeur de fonction) à la fonction au moment de l'exécution.

template <class T>
void doOperation(T t)
{
  int temp=0;
  t(temp);
  std::cout << "Result is " << temp << std::endl;
}

Il existe quelques considérations de performances mineures. Cette nouvelle version peut être moins efficace avec des arguments de pointeur de fonction car le pointeur de fonction particulier est uniquement déréférencé et appelé au moment de l'exécution tandis que votre modèle de pointeur de fonction peut être optimisé (éventuellement l'appel de fonction en ligne) en fonction du pointeur de fonction particulier utilisé. Les objets fonction peuvent souvent être développés de manière très efficace avec le modèle typé, bien que le particulier operator()soit complètement déterminé par le type de l'objet fonction.

CB Bailey
la source
1

La raison pour laquelle votre exemple de foncteur ne fonctionne pas est que vous avez besoin d'une instance pour appeler le operator().

Nikolai Fetissov
la source
0

Edit: Passer l'opérateur comme référence ne fonctionne pas. Pour plus de simplicité, comprenez-le comme un pointeur de fonction. Vous envoyez simplement le pointeur, pas une référence. Je pense que vous essayez d'écrire quelque chose comme ça.

struct Square
{
    double operator()(double number) { return number * number; }
};

template <class Function>
double integrate(Function f, double a, double b, unsigned int intervals)
{
    double delta = (b - a) / intervals, sum = 0.0;

    while(a < b)
    {
        sum += f(a) * delta;
        a += delta;
    }

    return sum;
}

. .

std::cout << "interval : " << i << tab << tab << "intgeration = "
 << integrate(Square(), 0.0, 1.0, 10) << std::endl;
AraK
la source