web-dev-qa-db-fra.com

Fonction de comparaison lambda C ++ 11 std :: set

Je veux créer un std::set Avec une fonction de comparaison personnalisée. Je pouvais le définir comme une classe avec operator(), mais je voulais profiter de la possibilité de définir un lambda où il est utilisé, j'ai donc décidé de définir la fonction lambda dans la liste d'initialisation du constructeur de la classe qui a le std::set comme membre. Mais je ne peux pas obtenir le type de lambda. Avant de continuer, voici un exemple:

class Foo
{
private:
     std::set<int, /*???*/> numbers;
public:
     Foo () : numbers ([](int x, int y)
                       {
                           return x < y;
                       })
     {
     }
};

J'ai trouvé deux solutions après la recherche: une, en utilisant std::function. Demandez au type de fonction de comparaison défini d'être std::function<bool (int, int)> et passez le lambda exactement comme je l'ai fait. La deuxième solution consiste à écrire une fonction make_set, comme std::make_pair.

SOLUTION 1:

class Foo
{
private:
     std::set<int, std::function<bool (int, int)> numbers;
public:
     Foo () : numbers ([](int x, int y)
                       {
                           return x < y;
                       })
     {
     }
};

SOLUTION 2:

template <class Key, class Compare>
std::set<Key, Compare> make_set (Compare compare)
{
     return std::set<Key, Compare> (compare);
}

La question est, ai-je une bonne raison de préférer une solution à l'autre? Je préfère le premier car il utilise des fonctionnalités standard (make_set n'est pas une fonction standard), mais je me demande: l'utilisation de std::function Rend-elle le code (potentiellement) plus lent? Je veux dire, cela réduit-il les chances que le compilateur insère la fonction de comparaison, ou il devrait être suffisamment intelligent pour se comporter exactement comme s'il s'agissait d'un type de fonction lambda et non pas std::function (Je sais, dans ce cas ce ne peut pas être un type lambda, mais vous savez, je demande en général)?

(J'utilise GCC, mais j'aimerais savoir ce que font les compilateurs populaires en général)

RÉSUMÉ, APRÈS AVOIR OBTENU BEAUCOUP DE GRANDES RÉPONSES:

Si la vitesse est critique, la meilleure solution consiste à utiliser une classe avec operator() aka functor. Il est plus facile pour le compilateur d'optimiser et d'éviter toute indirection.

Pour une maintenance facile et une meilleure solution à usage général, en utilisant les fonctionnalités C++ 11, utilisez std::function. Il est toujours rapide (juste un peu plus lent que le foncteur, mais il peut être négligeable) et vous pouvez utiliser n'importe quelle fonction - std::function, Lambda, n'importe quel objet appelable.

Il y a aussi une option pour utiliser un pointeur de fonction, mais s'il n'y a pas de problème de vitesse, je pense que std::function Est mieux (si vous utilisez C++ 11).

Il y a une option pour définir la fonction lambda ailleurs, mais vous ne gagnez rien à la fonction de comparaison étant une expression lambda, car vous pourriez aussi bien en faire une classe avec operator() et l'emplacement de la définition ne le serait pas être la construction de toute façon.

Il existe d'autres idées, comme l'utilisation de la délégation. Si vous voulez une explication plus approfondie de toutes les solutions, lisez les réponses :)

Oui, un std::function introduit une indirection presque inévitable vers votre set. Alors que le compilateur peut toujours, en théorie, comprendre que toute utilisation du std::function de votre set implique de l'appeler sur un lambda qui est toujours exactement le même lambda, à la fois dur et extrêmement fragile.

Fragile, car avant que le compilateur puisse se prouver que tous les appels à ce std::function sont en fait des appels vers votre lambda, il doit prouver qu'aucun accès à votre std::set ne définit jamais le std::function à autre chose que votre lambda. Ce qui signifie qu'il doit rechercher toutes les routes possibles pour atteindre votre std::set dans toutes les unités de compilation et prouver qu'aucune ne le fait.

Cela peut être possible dans certains cas, mais des modifications relativement inoffensives peuvent le casser même si votre compilateur réussit à le prouver.

D'un autre côté, un foncteur avec une operator() sans état a un comportement facile à prouver, et des optimisations impliquant ce sont des choses de tous les jours.

Donc oui, en pratique, je soupçonne que std::function pourrait être plus lent. D'un autre côté, la solution std::function est plus facile à maintenir que celle de make_set, et l'échange de temps de programmation pour les performances du programme est assez fongible.

make_set a le sérieux inconvénient que tout type de set doit être déduit de l'appel à make_set. Souvent, un set stocke un état persistant, et pas quelque chose que vous créez sur la pile, puis laissez tomber hors de portée.

Si vous avez créé une lambda auto MyComp = [](A const&, A const&)->bool { ... } statique ou globale sans état, vous pouvez utiliser la syntaxe std::set<A, decltype(MyComp)> pour créer un set qui peut persister, mais il est facile pour le compilateur d'optimiser (car toutes les instances de decltype(MyComp) sont des foncteurs sans état) et en ligne. Je le signale, car vous collez le set dans un struct. (Ou votre compilateur prend-il en charge

struct Foo {
  auto mySet = make_set<int>([](int l, int r){ return l<r; });
};

ce que je trouverais surprenant!)

Enfin, si vous êtes préoccupé par les performances, considérez que std::unordered_set est beaucoup plus rapide (au prix de ne pas pouvoir parcourir le contenu dans l'ordre et d'avoir à écrire/trouver un bon hachage), et qu'un std::vector trié est mieux si vous avoir un "insérer tout" en 2 phases puis "interroger le contenu à plusieurs reprises". Remplissez-le simplement dans le vector d'abord, puis sortuniqueerase, puis utilisez l'algorithme gratuit equal_range.

23

Il est peu probable que le compilateur puisse incorporer un appel à la fonction std ::, alors que tout compilateur qui prend en charge lambdas inclurait presque certainement la version du foncteur, y compris si ce foncteur est un lambda non masqué par un std::function.

Vous pouvez utiliser decltype pour obtenir le type de comparateur lambda:

#include <set>
#include <iostream>
#include <iterator>
#include <algorithm>

int main()
{
   auto comp = [](int x, int y){ return x < y; };
   auto set  = std::set<int,decltype(comp)>( comp );

   set.insert(1);
   set.insert(10);
   set.insert(1); // Dupe!
   set.insert(2);

   std::copy( set.begin(), set.end(), std::ostream_iterator<int>(std::cout, "\n") );
}

Qui imprime:

1
2
10

Regardez-le en direct Colir.

28
metal

Un lambda sans état (c'est-à-dire sans capture) peut se désintégrer en un pointeur de fonction, donc votre type pourrait être:

std::set<int, bool (*)(int, int)> numbers;

Sinon, j'irais pour le make_set Solution. Si vous n'utilisez pas de fonction de création sur une seule ligne parce qu'elle n'est pas standard, vous n'obtiendrez pas beaucoup de code écrit!

5
Jonathan Wakely

D'après mon expérience avec le profileur, le meilleur compromis entre performance et beauté est d'utiliser une implémentation déléguée personnalisée, telle que:

https://codereview.stackexchange.com/questions/14730/impossably-fast-delegate-in-c11

Comme le std::function est généralement un peu trop lourd. Je ne peux pas commenter vos circonstances spécifiques, car je ne les connais pas, cependant.

1
user1095108

Si vous êtes déterminé à avoir set comme membre de la classe, initialisant son comparateur au moment du constructeur, alors au moins un niveau d'indirection est inévitable. Considérez que pour autant que le compilateur le sache, vous pouvez ajouter un autre constructeur:

 Foo () : numbers ([](int x, int y)
                   {
                       return x < y;
                   })
 {
 }

 Foo (char) : numbers ([](int x, int y)
                   {
                       return x > y;
                   })
 {
 }

Une fois que vous avez un objet de type Foo, le type de set ne porte pas d'informations sur le constructeur qui a initialisé son comparateur, donc pour appeler le lambda correct, il faut une indirection vers le run- heure sélectionnée lambda operator().

Puisque vous utilisez des lambdas sans capture, vous pouvez utiliser le type de pointeur de fonction bool (*)(int, int) comme type de comparateur, car les lambdas sans capture ont la fonction de conversion appropriée. Cela impliquerait bien sûr une indirection via le pointeur de fonction.

1
ecatmur

La différence dépend fortement des optimisations de votre compilateur. S'il optimise lambda dans un std::function ceux-ci sont équivalents, sinon vous introduisez une indirection dans la première que vous n'aurez pas dans la seconde.

0
filmor