web-dev-qa-db-fra.com

Fonction transmise comme argument de modèle

Je recherche les règles impliquant la transmission d'arborescence de modèles C++.

Ceci est supporté 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>();
}

L'apprentissage de cette technique est cependant difficile. googler pour "fonction en tant qu'argument de modèle" ne mène pas à beaucoup. Et le classique Modèles complets C++: le guide complet étonnamment, il n’en parle pas non plus (du moins pas à partir de ma recherche).

Les questions que j'ai sont de savoir si cela est valide C++ (ou juste une extension largement pris en charge).

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

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

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

    doOperation<add3>();

Des pointeurs vers un lien Web ou deux, ou une page dans le livre de modèles C++ seraient appréciés!

215
SPWorley

Oui, c'est valide.

Pour que cela fonctionne aussi avec les foncteurs, la solution habituelle ressemble à ceci:

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());

Le problème avec ceci est que si cela complique la tâche du compilateur pour intégrer l'appel à add2, car tout ce que le compilateur sait, c'est qu'un type de pointeur de fonction void (*)(int &) est en train d'être passé à doOperation. (Mais add3, étant un foncteur, peut être facilement inséré. Ici, le compilateur sait qu'un objet de type add3 est transmis à la fonction, ce qui signifie que la fonction à appeler est add3::operator(), et non juste un pointeur de fonction inconnu.)

120
jalf

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

La manière "traditionnelle" de modéliser un morceau de code en C++ consiste à utiliser un foncteur, c'est-à-dire que le code est 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 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 à l'affaire 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 intégrer pleinement ce cas. (Je ne l'exclure pas cependant, car l'optimisation du compilateur est assez avancée.)

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

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

est toujours légal, et il est clair que cela n’est pas en ligne. Pour obtenir une intégration complète, nous avons besoin d'un modèle par valeur afin que la fonction soit 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. Nous nous attendons donc à ce que le code pour do_op ressemble beaucoup à "return a + b". (Programmeurs LISP, arrêtez votre sourire!)

Nous pouvons également confirmer que cela correspond plus à ce que nous voulons, car ceci:

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

échouera à compiler. 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é assez d'informations au moment de la compilation pour savoir ce qu'est notre op.

Donc, si le deuxième exemple est vraiment complet, et que le premier ne l’est pas, à quoi sert le modèle? Qu'est-ce que ça fait? La réponse est: type coercition. 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 suis pas en train de suggérer que c'est du bon C++ mais ...) Ce qui s'est passé, c'est que do_op a été modélisé autour du signatures des différentes fonctions, et chaque instanciation distincte va écrire un code de coercition de type différent. Donc, le code instancié pour do_op avec fadd ressemble à ceci:

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.

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

64
Ben Supnik

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 comme fonctions plutôt que comme pointeur à fonction. Au modèle instanciation 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 voulez passer un type de foncteur comme argument de template:

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 functor 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 cet aspect uniforme avec un argument de modèle est de définir do_op deux 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 vraiment m'attendais à ce que cela ne soit pas compilé, mais cela a fonctionné pour moi avec gcc-4.8 et Visual Studio 2013.

15
Kietz

Dans votre template

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

Le paramètre T est un paramètre de modèle non typé. Cela signifie que le comportement de la fonction template change avec la valeur du paramètre (qui doit être fixé au moment de la compilation, ce que sont les constantes du pointeur de la fonction).

Si vous voulez quelque chose qui fonctionne à la fois avec des objets et des paramètres de fonction, vous avez besoin d'un modèle typé. Cependant, dans ce cas, vous devez également fournir une instance d'objet (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 y a quelques considérations de performances mineures. Cette nouvelle version peut être moins efficace avec les arguments de pointeur de fonction, car le pointeur de fonction particulier est uniquement déréfermé 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 utilisé. Les objets de fonction peuvent souvent être très efficacement développés avec le modèle typé, bien que le operator() particulier soit complètement déterminé par le type de l'objet de fonction.

8
CB Bailey

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

1
Nikolai Fetissov

Edit: Passer l'opérateur en tant que référence ne fonctionne pas. Pour plus de simplicité, comprenez-le comme un pointeur de fonction. Vous envoyez juste 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;
0
AraK