web-dev-qa-db-fra.com

std :: set avec un type défini par l'utilisateur, comment garantir l'absence de doublons

J'ai donc un std :: set qui doit garder un ordre spécifique ainsi que ne pas autoriser les doublons d'un type défini par l'utilisateur (par moi). Maintenant, je peux obtenir l'ordre de fonctionner correctement en surchargeant l'opérateur "<" dans mon type. Cependant, l'ensemble ne détecte pas correctement les doublons, et pour être honnête, je ne suis pas tout à fait sûr de la façon dont il le fait en interne. J'ai surchargé l'opérateur '==', mais je ne suis pas certain que c'est ce que l'ensemble utilise réellement? La question est donc de savoir comment l'ensemble détermine les doublons lorsque vous ajoutez des valeurs? Voici le code pertinent:

Le type défini par l'utilisateur:

//! An element used in the route calculation.
struct RouteElem {
    int shortestToHere; // Shortest distance from the start.
    int heuristic;      // The heuristic estimate to the goal.
    Coordinate position;
    bool operator<( const RouteElem& other ) const
    {
        return (heuristic+shortestToHere) < (other.heuristic+other.shortestToHere);
    }
    bool operator==( const RouteElem& other ) const
    {
        return (position.x == other.position.x && position.y == other.position.y);
    }
};

Les éléments sont donc équivalents lorsque leur position est équivalente, et un élément est inférieur à un autre si sa fonction combinée est inférieure à celle de l'autre. Le tri fonctionne, mais l'ensemble accepte deux éléments de la même position.

58
DeusAduro

operator== N'est pas utilisé par std::set. Les éléments a et b sont considérés comme égaux si !(a < b) && !(b < a)

102
Paul

std::set prend en charge la spécification d'une fonction de comparaison. La valeur par défaut est less qui utilisera operator < pour vérifier l'égalité. Vous pouvez définir une fonction personnalisée pour vérifier l'égalité et l'utiliser à la place:

std::set<RouteElem, mycomparefunction> myset; 

Notez qu'il n'est pas possible de séparer la fonction de comparaison de la fonction de tri. std::set est un arbre binaire et si un élément dans un arbre binaire n'est ni plus grand ni plus petit qu'un élément spécifique, il devrait être au même endroit. Il fait quelque chose comme ça dans l'algorithme de recherche de place:

if (a < b) {
    // check the left subtree
} else if (b < a) {
    // check the right subtree
} else {
    // the element should be placed here.
}
31
Mehrdad Afshari

le comparateur de rlbond n'empêche pas l'insertion d'éléments comparables. Apparemment, il est difficile de le prouver dans les commentaires, étant donné la limite de caractères, car rlbond semble penser que std :: set garantit qu'il ne contiendra jamais deux éléments avec !compare(a,b) && !compare(b,a) pour son comparateur. Cependant, le comparateur de rlbond ne définit pas un ordre strict et n'est donc pas un paramètre valide pour std :: set.

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

struct BrokenOrder {
    int order;
    int equality;

    public:
    BrokenOrder(int o, int e) : order(o), equality(e) {}

    bool operator<(const BrokenOrder &rhs) const {
        return order < rhs.order;
    }
    bool operator==(const BrokenOrder &rhs) const {
        return equality == rhs.equality;
    }
};

std::ostream &operator<<(std::ostream &stream, const BrokenOrder &b) {
    return stream << b.equality;
}

// rlbond's magic comparator
struct LessThan : public std::binary_function<BrokenOrder, BrokenOrder, bool> {
    bool operator()(const BrokenOrder& lhs, const BrokenOrder& rhs) const
    {
        return !(lhs == rhs) && (lhs < rhs);
    }
};

int main() {
    std::set<BrokenOrder,LessThan> s;
    for (int i = 0; i < 5; ++i) {
        s.insert(BrokenOrder(i,i));
    }
    for (int i = 0; i < 5; ++i) {
        s.insert(BrokenOrder(10-i,i));
    }
    std::copy(s.begin(), s.end(), 
        std::ostream_iterator<BrokenOrder>(std::cout, "\n"));
}

Sortie:

0
1
2
3
4
3
2
1
0

Doublons. Le comparateur magique a échoué. Différents éléments de l'ensemble ont la même valeur de equality, et donc comparent la même chose avec operator==, Car lors de l'insertion, l'ensemble n'a jamais comparé le nouvel élément avec son doublon. Le seul doublon qui a été exclu était 4, car les deux 4 avaient des ordres de tri 4 et 6. Cela les a rapprochés suffisamment dans l'ensemble pour être comparés.

D'après la norme C++: 25.3: 3 "Pour que les algorithmes fonctionnent correctement, comp doit induire un ordre faible strict sur les valeurs".

25.3: 4 "... les exigences sont que comp et equiv soient toutes deux des relations transitives:

comp(a,b) && comp(b,c) implies comp(a,c)"

Maintenant, considérons les éléments a = BrokenOrder(1,1), b = BrokenOrder(2,2) et c = BrokenOrder(9,1) et comp bien sûr égaux au comparateur magique. Ensuite:

  • comp(a,b) est vrai car 1! = 2 (égalité) et 1 <2 (ordre)
  • comp(b,c) est vrai car 2! = 1 (égalité) et 2 <9 (ordre)
  • comp(a,c) est faux puisque 1 == 1 (égalité)
7
Steve Jessop

L'implémentation de l'ensemble STL fait quelque chose de conceptuel comme ceci pour détecter l'égalité:

bool equal = !(a < b) && !(b < a);

Autrement dit, si deux éléments ne sont pas tous deux inférieurs à l'autre, ils doivent être égaux. Vous pouvez être en mesure de vérifier cela en définissant un point d'arrêt sur votre méthode operator==() et en vérifiant si elle est jamais appelée du tout.

Je me méfierais généralement des opérateurs de comparaison qui vérifient des choses complètement différentes. Votre opérateur < Est défini en termes de deux choses distinctes de la façon dont votre opérateur == Est défini. En règle générale, vous souhaiterez que ces comparaisons utilisent des informations cohérentes.

4
Greg Hewgill

Vous pouvez essayer quelque chose comme ceci:

//! An element used in the route calculation.
struct RouteElem {
    int shortestToHere; // Shortest distance from the start.
    int heuristic;              // The heuristic estimate to the goal.
    Coordinate position;
    bool operator<( const RouteElem& other ) const
    {
      return (heuristic+shortestToHere) < (other.heuristic+other.shortestToHere);
    }
    bool operator==( const RouteElem& other ) const
    {
      return (position.x == other.position.x && position.y == other.position.y);
    }
};

struct CompareByPosition {
    bool operator()(const RouteElem &lhs, const RouteElem &rhs) {
        if (lhs.position.x != rhs.position.x) 
            return lhs.position.x < rhs.position.x;
        return lhs.position.y < rhs.position.y;
    }
};

// first, use std::set to remove duplicates
std::set<RouteElem,CompareByPosition> routeset;
// ... add each RouteElem to the set ...

// now copy the RouteElems into a vector
std::vector<RouteElem> routevec(routeset.begin(), routeset.end());

// now sort via operator<
std::sort(routevec.begin(), routevec.end());

Évidemment, il y a la copie au milieu, qui semble lente. Mais toute structure qui indexe les articles selon deux critères différents va donc avoir une sorte de surcharge supplémentaire par article par rapport à un ensemble. L'ensemble du code ci-dessus est O (n log n), en supposant que votre implémentation de std :: sort utilise introsort.

Si vous l'avez, sous ce schéma, vous pouvez utiliser unordered_set au lieu de set pour effectuer l'unification initiale. Étant donné que le hachage ne devrait dépendre que de x et y, il devrait être plus rapide que les comparaisons O (log N) nécessaires pour insérer dans un ensemble.

Edit: vient de remarquer que vous avez dit que vous vouliez "garder" l'ordre de tri, pas que vous vouliez tout traiter dans un lot. Désolé pour ça. Si vous souhaitez maintenir efficacement l'ordre et exclure les doublons lors de l'ajout d'éléments, je recommanderais d'utiliser l'ensemble ou l'ensemble non ordonné que je définis ci-dessus, en fonction de la position, ainsi qu'un std::multiset<RouteElem>, qui conservera le operator< ordre. Pour chaque nouvel élément, faites:

if (routeset.insert(elem).second) {
    routemultiset.insert(elem);
}

Mais attention, cela n'offre aucune garantie d'exception. Si le deuxième insert est lancé, l'ensemble de routes a été modifié, de sorte que l'état n'est plus cohérent. Donc je suppose que vous avez vraiment besoin de:

if (routeset.insert(elem).second) {
    try {
        routemultiset.insert(elem); // I assume strong exception guarantee
    } catch(...) {
        routeset.erase(elem); // I assume nothrow. Maybe should check those.
        throw;
    }
}

Ou un équivalent avec RAII, qui sera plus détaillé s'il n'y a qu'un seul endroit dans votre code que vous utilisez jamais la classe RAII, mais mieux s'il y a beaucoup de répétition.

2
Steve Jessop

Méfiez-vous des ramifications de cela. Il semble que vous essayez de faire quelque chose comme A *, et si vous essayez d'insérer un "doublon", il sera ignoré, même s'il existe un "meilleur" itinéraire.

REMARQUE: cette solution ne fonctionne pas, voir l'explication de onebyone ci-dessous

struct RouteElem 
{
    int shortestToHere; // Shortest distance from the start.
    int heuristic;              // The heuristic estimate to the goal.
    Coordinate position;
    bool operator<( const RouteElem& other ) const
    {
        return (heuristic+shortestToHere) < (other.heuristic+other.shortestToHere);
    }
    bool operator==( const RouteElem& other ) const
    {
        return (position.x == other.position.x && position.y == other.position.y);
    }
};

struct RouteElemLessThan : public std::binary_function<RouteElem, RouteElem, bool>
{
    bool operator()(const RouteElem& lhs, const RouteElem& rhs) const
    {
        return !(lhs == rhs) && (lhs < rhs);
    }
};

std::set<RouteElem, RouteElemLessThan> my_set;
0
rlbond