web-dev-qa-db-fra.com

Pourquoi utiliser des foncteurs sur des fonctions?

Comparer

double average = CalculateAverage(values.begin(), values.end());

avec

double average = std::for_each(values.begin(), values.end(), CalculateAverage());

Quels sont les avantages d'utiliser un foncteur par rapport à une fonction? Le premier n'est-il pas beaucoup plus facile à lire (même avant l'ajout de l'implémentation)?

Supposons que le foncteur est défini comme ceci:

class CalculateAverage
{
private:
   std::size_t num;
   double sum;
public:

   CalculateAverage() : num (0) , sum (0)
   {
   }

   void operator () (double elem) 
   {
      num++; 
      sum += elem;
   }

   operator double() const
   {
       return sum / num;
   }
};
54
DanDan

Au moins quatre bonnes raisons:

Séparation des préoccupations

Dans votre exemple particulier, l'approche basée sur les foncteurs a l'avantage de séparer la logique d'itération de la logique de calcul de moyenne. Vous pouvez donc utiliser votre foncteur dans d'autres situations (pensez à tous les autres algorithmes de la STL), et vous pouvez utiliser d'autres foncteurs avec for_each.

Paramétrage

Vous pouvez paramétrer un foncteur plus facilement. Ainsi, par exemple, vous pourriez avoir un foncteur CalculateAverageOfPowers qui prend la moyenne des carrés, ou cubes, etc. de vos données, qui s'écrirait ainsi:

class CalculateAverageOfPowers
{
public:
    CalculateAverageOfPowers(float p) : acc(0), n(0), p(p) {}
    void operator() (float x) { acc += pow(x, p); n++; }
    float getAverage() const { return acc / n; }
private:
    float acc;
    int   n;
    float p;
};

Vous pouvez bien sûr faire la même chose avec une fonction traditionnelle, mais cela rend son utilisation difficile avec les pointeurs de fonction, car elle a un prototype différent de CalculateAverage.

État

Et comme les foncteurs peuvent être dynamiques, vous pouvez faire quelque chose comme ceci:

CalculateAverage avg;
avg = std::for_each(dataA.begin(), dataA.end(), avg);
avg = std::for_each(dataB.begin(), dataB.end(), avg);
avg = std::for_each(dataC.begin(), dataC.end(), avg);

de faire la moyenne sur plusieurs ensembles de données différents.

Notez que presque tous les algorithmes/conteneurs STL qui acceptent les foncteurs exigent qu'ils soient des prédicats "purs", c'est-à-dire qu'ils n'ont pas de changement d'état observable dans le temps. for_each est un cas particulier à cet égard (voir par exemple Bibliothèque standard C++ efficace - for_each vs transform ).

Performances

Les foncteurs peuvent souvent être intégrés par le compilateur (la STL est un tas de modèles, après tout). Bien que la même chose soit théoriquement vraie pour les fonctions, les compilateurs ne s'alignent généralement pas via un pointeur de fonction. L'exemple canonique est de comparer std::sort vs qsort; la version STL est souvent 5 à 10 fois plus rapide, en supposant que le prédicat de comparaison lui-même est simple.

Résumé

Bien sûr, il est possible d'émuler les trois premiers avec des fonctions et des pointeurs traditionnels, mais cela devient beaucoup plus simple avec les foncteurs.

75

Avantages des foncteurs:

  • Contrairement aux fonctions, Functor peut avoir un état.
  • Functor s'intègre dans le paradigme OOP par rapport aux fonctions.
  • Le foncteur peut souvent être aligné contrairement aux pointeurs de fonction
  • Functor ne nécessite pas de répartition vtable et d'exécution, et donc plus efficace dans la plupart des cas.
9
Alok Save

std::for_each est facilement le plus capricieux et le moins utile des algorithmes standard. C'est juste un joli emballage pour une boucle. Cependant, même cela a des avantages.

Considérez à quoi doit ressembler votre première version de CalculateAverage. Il aura une boucle sur les itérateurs, puis fera des trucs avec chaque élément. Que se passe-t-il si vous écrivez mal cette boucle? Oups; il y a une erreur de compilation ou d'exécution. La deuxième version ne peut jamais avoir de telles erreurs. Oui, ce n'est pas beaucoup de code, mais pourquoi devons-nous écrire des boucles si souvent? Pourquoi pas une seule fois?

Maintenant, considérons les algorithmes réels; ceux qui fonctionnent réellement. Voulez-vous écrire std::sort? Ou std::find? Ou std::nth_element? Savez-vous même comment le mettre en œuvre de la manière la plus efficace possible? Combien de fois souhaitez-vous implémenter ces algorithmes complexes?

Quant à la facilité de lecture, c'est aux yeux du spectateur. Comme j'ai dit, std::for_each n'est guère le premier choix pour les algorithmes (en particulier avec la syntaxe basée sur la plage de C++ 0x). Mais si vous parlez de vrais algorithmes, ils sont très lisibles; std::sort trie une liste. Certains des plus obscurs comme std::nth_element ne sera pas aussi familier, mais vous pouvez toujours le rechercher dans votre référence C++ pratique.

Et même std :: for_each est parfaitement lisible une fois que vous utilisez Lambda en C++ 0x.

7
Nicol Bolas

• Contrairement aux fonctions, Functor peut avoir un état.

Ceci est très intéressant car std :: binary_function, std :: less et std :: equal_to a un modèle pour un opérateur () qui est const. Mais que faire si vous vouliez imprimer un message de débogage avec le nombre d'appels en cours pour cet objet, comment feriez-vous?

Voici le modèle pour std :: equal_to:

struct equal_to : public binary_function<_Tp, _Tp, bool>
{
  bool
  operator()(const _Tp& __x, const _Tp& __y) const
  { return __x == __y; }
};

Je peux penser à 3 façons de permettre à l'opérateur () d'être const, tout en modifiant une variable membre. Mais quelle est la meilleure façon? Prenez cet exemple:

#include <iostream>
#include <string>
#include <algorithm>
#include <functional>
#include <cassert>  // assert() MACRO

// functor for comparing two integer's, the quotient when integer division by 10.
// So 50..59 are same, and 60..69 are same.
// Used by std::sort()

struct lessThanByTen: public std::less<int>
{
private:
    // data members
    int count;  // nr of times operator() was called

public:
    // default CTOR sets count to 0
    lessThanByTen() :
        count(0)
    {
    }


    // @override the bool operator() in std::less<int> which simply compares two integers
    bool operator() ( const int& arg1, const int& arg2) const
    {
        // this won't compile, because a const method cannot change a member variable (count)
//      ++count;


        // Solution 1. this trick allows the const method to change a member variable
        ++(*(int*)&count);

        // Solution 2. this trick also fools the compilers, but is a lot uglier to decipher
        ++(*(const_cast<int*>(&count)));

        // Solution 3. a third way to do same thing:
        {
        // first, stack copy gets bumped count member variable
        int incCount = count+1;

        const int *iptr = &count;

        // this is now the same as ++count
        *(const_cast<int*>(iptr)) = incCount;
        }

        std::cout << "DEBUG: operator() called " << count << " times.\n";

        return (arg1/10) < (arg2/10);
    }
};

void test1();
void printArray( const std::string msg, const int nums[], const size_t ASIZE);

int main()
{
    test1();
    return 0;
}

void test1()
{
    // unsorted numbers
    int inums[] = {33, 20, 10, 21, 30, 31, 32, 22, };

    printArray( "BEFORE SORT", inums, 8 );

    // sort by quotient of integer division by 10
    std::sort( inums, inums+8, lessThanByTen() );

    printArray( "AFTER  SORT", inums, 8 );

}

//! @param msg can be "this is a const string" or a std::string because of implicit string(const char *) conversion.
//! print "msg: 1,2,3,...N", where 1..8 are numbers in nums[] array

void printArray( const std::string msg, const int nums[], const size_t ASIZE)
{
    std::cout << msg << ": ";
    for (size_t inx = 0; inx < ASIZE; ++inx)
    {
        if (inx > 0)
            std::cout << ",";
        std::cout << nums[inx];
    }
    std::cout << "\n";
}

Étant donné que les 3 solutions sont compilées, elles incrémentent le nombre de 3. Voici le résultat:

gcc -g -c Main9.cpp
gcc -g Main9.o -o Main9 -lstdc++
./Main9
BEFORE SORT: 33,20,10,21,30,31,32,22
DEBUG: operator() called 3 times.
DEBUG: operator() called 6 times.
DEBUG: operator() called 9 times.
DEBUG: operator() called 12 times.
DEBUG: operator() called 15 times.
DEBUG: operator() called 12 times.
DEBUG: operator() called 15 times.
DEBUG: operator() called 15 times.
DEBUG: operator() called 18 times.
DEBUG: operator() called 18 times.
DEBUG: operator() called 21 times.
DEBUG: operator() called 21 times.
DEBUG: operator() called 24 times.
DEBUG: operator() called 27 times.
DEBUG: operator() called 30 times.
DEBUG: operator() called 33 times.
DEBUG: operator() called 36 times.
AFTER  SORT: 10,20,21,22,33,30,31,32
2
joe

Dans la première approche, le code d'itération doit être dupliqué dans toutes les fonctions qui veulent faire quelque chose avec la collection. La seconde approche masque les détails de l'itération.

2
Vijay Mathew

Vous comparez des fonctions à différents niveaux d'abstraction.

Vous pouvez implémenter CalculateAverage(begin, end) comme:

template<typename Iter>
double CalculateAverage(Iter begin, Iter end)
{
    return std::accumulate(begin, end, 0.0, std::plus<double>) / std::distance(begin, end)
}

ou vous pouvez le faire avec une boucle for

template<typename Iter>
double CalculateAverage(Iter begin, Iter end)
{
    double sum = 0;
    int count = 0;
    for(; begin != end; ++begin) {
        sum += *begin;
        ++count;
    }
    return sum / count;
}

Le premier vous oblige à en savoir plus, mais une fois que vous les connaissez, c'est plus simple et laisse moins de possibilités d'erreur.

Il utilise également uniquement deux composants génériques (std::accumulate et std::plus), ce qui est également souvent le cas dans des cas plus complexes. Vous pouvez souvent avoir un foncteur universel simple (ou une fonction; une ancienne fonction ordinaire peut agir comme foncteur) et simplement le combiner avec l'algorithme dont vous avez besoin.

1
Jan Hudec

OOP est le mot clé ici.

http://www.newty.de/fpt/functor.html :

4.1 Que sont les foncteurs?

Les foncteurs sont des fonctions avec un état. En C++, vous pouvez les réaliser en tant que classe avec un ou plusieurs membres privés pour stocker l'état et avec un opérateur surchargé () pour exécuter la fonction. Les foncteurs peuvent encapsuler des pointeurs de fonctions C et C++ en utilisant les modèles de concepts et le polymorphisme. Vous pouvez créer une liste de pointeurs vers des fonctions membres de classes arbitraires et les appeler tous via la même interface sans se soucier de leur classe ou de la nécessité d'un pointeur vers une instance. Toutes les fonctions doivent simplement avoir le même type de retour et les mêmes paramètres d'appel. Parfois, les foncteurs sont également appelés fermetures. Vous pouvez également utiliser des foncteurs pour implémenter des rappels.