【问题标题】:Custom comparator to insert unique elements into a set in c++自定义比较器以将唯一元素插入 C++ 中的集合
【发布时间】:2016-04-07 15:56:45
【问题描述】:

我有以下代码 sn-p 用于将元素插入集合并检索它们。但正如您从示例输出中看到的那样,不知何故,学生 1 的姓名(即“stud1”)并未打印出来,即使它是按他们的到达时间排序的。谁能帮忙弄清楚这种方法有什么问题?

学生.h

#ifndef Student_h
#define Student_h

#include "string"

class Student
{

public:
    Student();
    ~Student();

    void setName(const std::string& p_name)     { _name = p_name; }
    void setArrivalTime(const int p_arr_t)      { _arrivalTime = p_arr_t; }

    const std::string& getName() const         { return _name; }
    const int getArrivalTime() const           { return _arrivalTime; }

private:
    std::string _name;
    int _arrivalTime;

};

struct CompareStudByArrivaltime
{
    const bool operator()(const Student* s1, const Student* s2) const;
};

#endif /* Student_h */

学生.cpp

#include <stdio.h>
#include "Student.h"

Student::Student()
{

}

Student::~Student()
{

}

const bool CompareStudByArrivaltime::operator()(const Student* s1, const Student* s2) const
{

    if (s1->getName() == s2->getName())
    {
        return false;
    }

    return (s1->getArrivalTime() <= s2->getArrivalTime());
}

main.cpp

#include <iostream>
#include <set>
#include <map>
#include <vector>

#include "Student.h"

typedef std::vector<Student> StudentsPool;
typedef std::set<Student*, CompareStudByArrivaltime> Students;
typedef std::map<std::string,Students> SchoolStudentsMap;

SchoolStudentsMap g_school_studs;
StudentsPool g_stud_pool;

Student* getStud(const std::string& n)
{
    for(StudentsPool::iterator itr = g_stud_pool.begin(); itr != g_stud_pool.end(); ++itr)
    {
        if (itr->getName() == n)
        {
            return &(*itr);
        }
    }

    return NULL;
}

void initObj()
{
    /** School 1 Record */
    std::string school_name = "school1";

    char c1 [] = {'s','t','u','d','1','\0'};
    std::string n1(c1);
    //Student* s1 = new Student();
    Student s1;
    s1.setName(n1);
    s1.setArrivalTime(10);
    g_stud_pool.push_back(s1);

    Student* tmp = NULL;

    tmp = getStud("stud1");
    g_school_studs[school_name].insert(tmp);

    char c2 [] = {'s','t','u','d','2','\0'};
    std::string n2(c2);
    Student s2;
    s2.setName(n2);
    s2.setArrivalTime(2);
    g_stud_pool.push_back(s2);

    tmp = getStud("stud2");
    g_school_studs[school_name].insert(tmp);

    char c3 [] = {'s','t','u','d','3','\0'};
    std::string n3(c3);
    Student s3;
    s3.setName(n3);
    s3.setArrivalTime(5);
    g_stud_pool.push_back(s3);

    tmp = getStud("stud3");
    g_school_studs[school_name].insert(tmp);
}

void processObj()
{
    for(SchoolStudentsMap::iterator itr = g_school_studs.begin(); itr != g_school_studs.end(); ++itr)
    {
        Students& studs = itr->second;

        for(Students::iterator sitr = studs.begin(); sitr != studs.end(); ++sitr)
        {
            Student* s = (*sitr);
            std::cerr << "Name: " << s->getName() << ", Arr Time: " << s->getArrivalTime() << std::endl;
        }
    }

}

int main(int argc, const char * argv[])
{

    initObj();
    processObj();

    return 0;
}

样本输出

Name: stud2, Arr Time: 2
Name: stud3, Arr Time: 5
Name: , Arr Time: 10

【问题讨论】:

    标签: c++ gcc set std comparator


    【解决方案1】:

    看看你的比较函数。如果到达时间相同,您将返回true,但物品的顺序不同。

    const bool CompareStudByArrivaltime::operator()(const Student* s1, const Student* s2) const
    {
        if (s1->getName() == s2->getName())
        {
            return false;
        }
    
        return (s1->getArrivalTime() <= s2->getArrivalTime());  
       // what if s1 and s2 are equal, but switched?  You still return true.
    }
    

    假设 s1 和 s2 到达时间相同。您的函数返回true。然后假设我们调用您的比较函数,但这次使用 s2 和 s1。你仍然返回true。这个怎么可能?怎么能说 s1 在 s2 之前放入容器中,那么同时 s2 应该在 s1 之前放入容器中呢?编译器问你哪个先出现,当项目相等时你给出了一个不可能的答案。这就是std::set 排序标准混淆的地方,最终会给出错误的结果。

    简而言之,这就是严格-弱排序的全部内容,@Slava 提供了解决方案的详细信息。

    顺便说一句,切换项目和检查返回值的测试是由调试 Visual C++ 运行时完成的。您的代码可能会立即断言,因为运行时调用排序例程两次,首先是s1, s2,然后是s2, s1。如果您在这两种情况下都返回了 true,则运行时会中止您的应用程序。


    另一个问题是您将指向 Student 向量中的项目的指针存储在此处:

    g_stud_pool.push_back(s1);
    tmp = getStud("stud1");  // <-- gets pointer to item just placed in g_stud_pool
    g_school_studs[school_name].insert(tmp);  // <-- pointer to Student from the vector being stored
    
    //...
    
    g_stud_pool.push_back(s2); // <-- invalidates previous pointer
    tmp = getStud("stud2");
    g_school_studs[school_name].insert(tmp); // <-- map now contains invalid pointer(s)
    

    您将项目添加到g_stud_pool 向量,然后立即使用指向您刚刚放置在向量中的项目的指针,通过将该指针放置在您的std::set 中来引用该项目。

    这样做的问题是,每次向向量中添加一个项目时,任何指向前一个项目的指针都可能会失效。最终发生的事情是您的set 使用的比较功能将使用已失效的地址。

    解决此问题的最快方法(不是唯一方法)是更改为在调整大小时不会使指针(和迭代器)失效的容器。这样的容器是std::list。所以改成这样:

    #include <list>
    typedef std::list<Student> StudentsPool;
    

    解决了失效问题,因为std::list 在调整列表大小时不会使指针和迭代器失效。

    Here is a live example

    【讨论】:

    • 按照您的建议,我进行了以下更改,但无助于解决问题。 "返回 (s1->getArrivalTime() getArrivalTime());"
    • 您正在存储指向向量中项目的指针。您不能这样做,因为每次增加向量的大小时,这些指针都会失效。
    • 感谢有用的建议!
    【解决方案2】:

    您的比较器不正确,因为它破坏了“严格的弱排序关系”,它应该是这样的:

    bool CompareStudByArrivaltime::operator()(const Student* s1, const Student* s2) const
    {
    
        if (s1->getName() != s2->getName())
        {
            return s1->getName() < s2->getName();
        }
    
        return (s1->getArrivalTime() < s2->getArrivalTime());
    }
    

    甚至更简单:

    bool CompareStudByArrivaltime::operator()(const Student* s1, const Student* s2) const
    {
       return std::make_tuple( s1->getName(), s1->getArrivalTime() ) <
              std::make_tuple( s2->getName(), s2->getArrivalTime() );
    }
    

    详情可见here

    【讨论】:

    • 我尝试了您的第一个建议,但未能解决问题。我仍然遇到类似的问题。
    • 比较错误的问题解决。这只是您在程序中遇到的问题之一,另一个问题很可能是滥用在向量中存储项目的指针。
    猜你喜欢
    • 1970-01-01
    • 2017-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-06
    • 1970-01-01
    相关资源
    最近更新 更多