【问题标题】:std::set doesn't detect duplicate custom objectsstd::set 未检测到重复的自定义对象
【发布时间】:2017-07-18 14:18:57
【问题描述】:

我有一张用于保存球队名称和球队球员std:pair<std::string, std::vector<std::string> > 的地图,以及一组指向球员的指针,以保持球员按胜利降序排列。还有一个问题——一名球员可以参加多支球队。

    class Player
{
public:
    int wins;
    std::string name;

    Player() {}
    Player(std::string name) : wins(0), name(name) {}

    bool operator<(const Player* rhs) const
    {
        return this->wins > rhs->wins;
    }

    bool operator()(const Player* lhs, const Player* rhs) const
    {
        return lhs->wins > rhs->wins;
    }
};

int main()
{
    // EXAMPLE
    std::set<Player*> players;
    std::map<std::string, std::vector<Player> > teams;

    teams["TeamA"].push_back(Player("a"));
    teams["TeamB"].push_back(Player("a"));;
    players.insert(&teams["TeamA"].front());
    players.insert(&teams["TeamB"].front());

    std::cout << players.size(); // output 2 

    return 0;
}

您可以看到“TeamA”和“TeamB”中的球员是相同的,但仍然在组合中添加了两个指针,我无法弄清楚为什么.. 有什么我遗漏的吗?

【问题讨论】:

    标签: c++ c++11 stdmap stdset


    【解决方案1】:

    您的集合包含指针,而不是 Player 对象。这意味着指针比较用于确定等价性。

    当然,TeamA 中第一个对象的地址与 TeamB 中第一个玩家的地址不同,即使它们包含相同的数据。

    如果要按指针进行比较,请使用自定义比较器类型:

    PlayerComp {
      bool operator()(Player* lhs, Player* rhs){
        return *lhs < *rhs;
      }
    };
    
    std::set<Player*, PlayerComp> players;
    

    而播放器类在它自己的实例上实现operator&lt;,而不是在指针上:

    bool operator<(const Player& rhs) const
    {
        return wins < rhs.wins;
    }
    

    【讨论】:

    • 好吧,如果自定义运算符被调用,我们会有一个更大的问题:如果一个玩家获胜?然后集合的键被修改-> UB ...
    • 注意指针比较是UB,除非所有指针都指向同一个数组或其他对象中的数据。碰巧,std::set 使用std::less 而不是指针比较,从而避免了这个问题。 ;)
    • @Yakk - 我的回答没有比较指针,只有指针。或者这是教育OP的一般性评论?
    • @StoryTeller 您的第一段有点误导,因为它声明“指针比较”(天真地)读为a &lt; b 而不是std::less&lt;T*&gt;{}(a, b)。这是一个非常次要的问题。更大的问题是,如果团队在被set 指出后获得/失去胜利,那么 OP 的设计是危险的。
    • @Yakk - 我明白了,感谢您指出。很快就会解决。
    【解决方案2】:

    如果我的假设是正确的,这将不会如你所愿!

    teams["TeamA"].push_back(Player("a"));
    teams["TeamB"].push_back(Player("a"));
    

    我在这里假设您希望一个名为“a”的单个玩家加入两个团队。如果该玩家在 A 队中获胜,则其 wins 属性会增加,就像在 B 队中获胜一样。

    不过,您实际上所做的是创建了两个不同的玩家,都称为“a”,他们都出现在系列中(嗯,实际上不是,见下文...),每个人都保持自己的胜利(第一玩家“a”在 A 队中获胜,第二名在 B 队中获胜)。

    必须做的是在球员集合中创建球员并将这些单个实例添加到各个球队的向量中:

    std::set<Player> players;
    std::map<std::string, std::vector<Player*> > teams;
    auto entry = players.insert(Player("a"));
    teams["TeamA"].push_back(&entry->first);
    

    但这也行不通:您的比较仅基于胜利!如果您输入两个不同的玩家(顺便说一下,如果您应用 StoryTeller 的修复,同样会发生!),两者都以 0 的胜利开始。所以两个玩家将比较相等(a &lt; bb &lt; a 两者都不会因此应用平等),所以只有一个玩家将进入集合...

    另外,std::set 没有“自动更新”功能!它要求其条目保持不变(至少其中的成员用于比较!)。因此,如果您在比赛期间更新您的胜利,您的集合中的排序将完全丢失(除此之外,修改地图或集合的键实际上是未定义的行为)。

    结论:std::set 不是一个选项,至少不是你想要的使用方式!

    让我建议你另一种方法:

    // yes, a vector...
    // we'll need to maintain sorting ourselves...
    std::vector<Player*> players;
    // need pointers here, too, as pointers to objects
    // (those within the teams!) might INVALIDATE on
    // vector<Player> resizing!!!
    
    std::map<std::string, std::vector<Player*>> teams;
    
    players.push_back(new Player("a"));
    teams["TeamA"].push_back(players.back());
    teams["TeamB"].push_back(players.back());
    
    players.push_back(new Player("b"));
    teams["TeamA"].push_back(players.back());
    teams["TeamC"].push_back(players.back());
    

    现在让我们一支球队获胜:

    for(auto p : teams["TeamA"])
    {
        ++p->wins;
    }
    

    好吧,我们只是采取措施:

    std::sort(players.begin(), players.end(), PlayerComp());
    

    嘿,这只是 StoryTeller 回答中的 PlayerComp... 或者我们使用 lambda 代替:

    std::sort
    (
        players.begin(), players.end(),
        [](Player const* x, Player const* y) { return *x < *y; }
    );
    

    最后,不要忘记再次删除你的播放器对象:

    for(auto p : players)
    {
        delete p;
    }
    

    如果你使用智能指针,你可以跳过删除,例如。 G。像这样:

    std::vector<std::unique_ptr<Player>> players;
    
    players.push_back(std::make_unique<Player>("a"));
    teams["TeamA"].push_back(players.back().get());
    
    for(auto p : teams["TeamA"])
        ++p->score;
    std::sort
    (
        players.begin(), players.end(),
        [](std::unique_ptr<Player> const& x, std::unique_ptr<Player> const& y) { return *x < *y }
    );
    

    以上假设球员的向量对球员对象具有唯一的所有权,就像我的非智能指针解决方案一样——因此,如果球员的向量被删除,球队的球员指针都会变得无效/悬空。适用于特定情况,std::shared_ptr 提供了一个值得一提的替代方案,它避免了悬空指针的可能性(代价是创建、分配和销毁期间的一些对象管理开销,但是,在访问智能指针时不是):

    std::vector<std::shared_ptr<Player>> players;
    std::map<std::string, std::vector<std::shared_ptr<Player>>> teams;
    
    players.push_back(std::make_shared<Player>("a"));
    teams["TeamA"].push_back(players.back());
    
    
    for(auto p : teams["TeamA"])
        ++p->score;
    std::sort
    (
            players.begin(), players.end(),
            [](std::shared_ptr<Player> const& x, std::shared_ptr<Player> const& y) { return *x < *y; }
    );
    

    【讨论】:

    • 我已经投了赞成票,这并不取决于以下内容。如果您要建议共享所有权,请提及 std::shared_ptr
    • @StoryTeller Ha,让我回来了……我的狭隘焦点:“自然,玩家的向量是唯一的所有者。”谢谢,很好的提示...
    • 嗯,我不是想“抓住你”。我试图提出改进建议。我必须说,那些对我的回答的抨击现在看起来完全不同了。
    • @StoryTeller 好吧,那你是偶然的 - 不,没关系,反正只是个玩笑......对你的昵称非常熟悉,很多优秀的 cmets,我很感激他们(而不仅仅是我自己的答案)。
    猜你喜欢
    • 2017-02-20
    • 2021-10-23
    • 2012-09-28
    • 1970-01-01
    • 2021-09-29
    • 2010-09-06
    • 2020-10-17
    • 1970-01-01
    相关资源
    最近更新 更多