自定义比较器,将唯一元素插入到c++中的集合中

Custom comparator to insert unique elements into a set in c++

本文关键字:c++ 集合 插入 元素 比较器 唯一 自定义      更新时间:2023-10-16

我有以下代码片段,用于将元素插入到集合中并检索它们。但正如您从样本输出中看到的,不知何故,学生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',''};
    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',''};
    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',''};
    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

看看您的比较函数。如果到达时间相同,但物品的顺序不同,则返回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在调整列表大小时不会使指针和迭代器无效。

下面是的一个实际示例

您的比较器是不正确的,因为它破坏了"严格的弱排序关系",它应该类似于:

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() );
}

详细信息可以在这里找到