如何使用c++ std::set作为类的构建块

How to use C++ std::sets as building blocks of a class?

本文关键字:构建 set 何使用 c++ std      更新时间:2023-10-16

我需要一个满足以下条件的数据结构:

  • 存储任意数量的元素,其中每个元素由10个数字度量来描述
  • 允许通过任何度量
  • 快速(log n)搜索元素
  • 允许快速(log n)插入新元素
  • 允许快速(log n)去除元素

让我们假设这些元素的构造代价很高。

我想出了下面的计划

  • 将所有元素存储在名为DATA的vector中。
  • 使用10个std::sets,每10个指标一个。每个std:set都是轻量级的,它只包含整数,这些整数是指向向量DATA的索引。比较运算符在DATA中"查找"适当的元素,然后选择适当的度量
template< int C >
struct Cmp
{
    bool operator() (int const a, int const b)
    {
        return ( DATA[a].coords[C] != DATA[b].coords[C] ) 
           ? ( DATA[a].coords[C] < DATA[b].coords[C] )
           : ( a < b );
    }
};
vector对象永远不能修改或删除

元素。将新元素推回DATA,然后将其索引(DATA.size()-1)插入到集合(set<int, Cmp<..> >)中。为了删除一个元素,我在元素中设置了一个标志,表示它已被删除(而不是实际从DATA vector中删除它),然后从所有十个std::set中删除元素索引。

只要DATA是全局变量,就可以正常工作。(它还使模板结构体Cmp依赖于一个全局变量,从而在某种程度上滥用了类型系统。)

然而,我无法将DATA向量和std::set (set<int, Cmp<...> >)包含在一个类中,然后用std::sets"索引"DATA。首先,在外部类内部定义的比较操作符Cmp不能访问外部类的字段(因此它不能评估DATA)。我也不能将vector传递给Cmp构造函数,因为Cmp是由std::set构造的,而std::set需要一个没有参数的构造函数的比较操作符。

我有一种感觉,我正在与c++类型系统对抗,并试图实现类型系统故意阻止我做的事情。(我试图使std::set依赖于仅在运行时构造的变量。)虽然我理解为什么类型系统可能不喜欢我所做的,但我认为这是一个合理的用例。

有没有一种方法来实现我上面描述的数据结构/类,而不提供std::set/红黑树的重新实现?我希望有一个我还没有想到的诀窍。(是的,我知道boost有一些东西,但我想坚持使用标准库。)

当我读到"通过值bar查找foo"时,我的第一反应是使用map<>或类似的东西。这里有一些暗示:

  1. std::map中的键(或std::set中的值)是唯一的,因此没有两个元素可以共享相同的键,因此没有两个数据对象能够具有相同的度量。如果多个数据对象可以具有相同的度量(这从你的问题中不清楚),使用std::multimap(或std::multiset)可以工作。
  2. 如果键是常量并且存储在元素本身中,使用set<data*,cmp>是一种常见的方法。然后比较器只从对象中检索相应字段并对它们进行比较。然后查找需要创建一个临时对象并对其使用find()。一些实现还具有允许使用不同类型进行搜索的扩展,这将使搜索变得更加容易,但也使移植需要实际的工作。作为键的字段保持不变是很重要的,因为如果你修改它们,你隐式地改变了set<>的顺序。这就是set<>的元素实际上是常量的原因,即即使是普通的iterator也有一个常量作为值类型。如果你存储指针,你可以很容易地解决这个问题,因为常量指针和指向常量的指针是不同的。别拿那个搬起石头砸自己的脚!
  3. 如果指标不是对象本身的属性(或者您不介意冗余存储它们),使用std::map将是一个自然的选择。根据度量,在多个键下存储相同的对象,可以在单独的容器中完成(map<int,data*> c[10];)。然而,你可以在单个地图中使用例如pair<metric,value>作为键(map<pair<int,int>,data*> c;)。
  4. 使用vector<>来存储实际的元素,并且只将它们作为指针或索引引用到map中肯定有效。不过我还是会选择指针,因为这是允许使用集合或映射的上述方法工作的原因。如果不这样做,比较器将不得不存储对容器的引用,而目前它只使用全局DATA容器。将这种方法用于向量是很棘手的,因为它会在增长时重新分配元素,正如您正确指出的那样。我会考虑不同的容器类型,如std::liststd::deque。前者也允许擦除元素,但是每个元素的开销更高。后者的每个元素开销相对较低,仅略高于std::vector。然后,您甚至可以存储迭代器而不是指针,这有助于调试,只要您为此使用"检查过的STL"。尽管如此,你仍然需要做一些手工记录,哪些对象仍然被引用,哪些对象没有。
  5. 除了使用单独的容器,您还可以动态地分配元素,尽管这本身有一些开销。如果每个元素的开销不是问题,那么可以使用引用计数的智能指针。如果应用程序是一次性进程,你也可以使用原始指针,让操作系统在退出时回收内存。

注意,我假设不可以存储数据对象的多个副本。如果是这种情况,您还可以使用map<int,data> m[10];,其中每个映射存储自己的数据对象副本。所有的记账问题都将得到解决,但代价是10倍的开销。