在STL map / set中end()是否必须是常量?

时间:2009-09-16 10:41:41

标签: c++ stl iterator

标准中的

§23.1.2.8声明对set / map的插入/删除操作不会使对这些对象的任何迭代器无效(除了指向已删除元素的迭代器)。

现在,请考虑以下情况:您希望实现具有唯一编号节点的图形,其中每个节点都具有固定数量(例如4个)的邻居。利用上述规则,你可以这样做:

class Node {
    private:
        // iterators to neighboring nodes
        std::map<int, Node>::iterator neighbors[4];
        friend class Graph;
};

class Graph {
    private:
        std::map<int, Node> nodes;
};

编辑:由于第4行中Node的不完整性(请参阅回复/评论),并非完全相同,但无论如何都是这样的。

这很好,因为这样你可以插入和删除节点而不会使结构的一致性失效(假设你跟踪删除并从每个节点的数组中删除删除的迭代器)。

但是,假设您还希望能够存储“无效”或“不存在”的邻居值。不用担心,我们可以使用nodes.end() ......或者我们可以吗?是否存在某种保证:{J} 1}在上午8点与nodes.end()相同,在数十亿次插入/删除后?也就是说,我可以安全nodes.end()将作为参数接收的迭代器与某些Graph方法中的==进行比较吗?

如果没有,这会有效吗?

nodes.end()

也就是说,我可以在构造时将class Graph { private: std::map<int, Node> nodes; std::map<int, Node>::iterator _INVALID; public: Graph() { _INVALID = nodes.end(); } }; 存储在变量中,然后在我想将邻居设置为无效状态时使用此变量,还是将其与方法中的参数进行比较?或者有可能在某个地方,指向现有对象的有效迭代器将比较等于nodes.end()

如果这也不起作用, 我可以为无效的邻居值留出空间吗?

10 个答案:

答案 0 :(得分:5)

你写(我强调):

  标准中的

23.1.2.8声明对set / map的插入/删除操作不会使任何迭代器无效(指向已删除元素的迭代器除外)。

实际上,23.1.2 / 8的文本有点不同(再次,我强调):

  

插入成员不应影响迭代器的有效性并将引用到容器,并且擦除成员应仅使迭代器和对已擦除元素的引用无效。

我读到这个:如果你有一个地图,并以某种方式获得一个迭代器到这个地图(再次:它没有说到地图中的一个对象),这个迭代器将保持有效尽管插入和删除元素。假设std::map<K,V>::end()获得“地图中的迭代器”,则不应通过插入/删除使其失效。

这当然留下了一个问题,即“未通过无效”意味着它总是具有相同的值。我个人的假设是没有具体说明。但是,为了使“无效”短语有意义,相同地图的std::map<K,V>::end()的所有结果必须始终相等,即使面对插入/删除:

my_map_t::iterator old_end = my_map.end();
// wildly change my_map
assert( old_end == my_map.end() ); 

我的解释是,如果old_end在对地图的更改(作为标准promisses)期间保持“有效”,那么该断言应该通过。

免责声明:我不是母语人士,并且非常很难消化那些可怕的神圣PDF版本。事实上,总的来说,我就像瘟疫一样避免它。

哦,我的第一个想法也是:学术POV的问题很有趣,但他为什么不简单地存储密钥而不是迭代器呢?

答案 1 :(得分:4)

23.1 / 7表示end()返回一个

的迭代器
  

是容器的过去值。

首先,它确认end()返回的是迭代器。其次,它说迭代器不指向特定元素。由于删除只能使指向某处的迭代器无效(对于要删除的元素),删除不能使end()无效。

答案 2 :(得分:3)

嗯,只要比较和这样的工作,就没有什么能阻止特定的集合实现让end()依赖于集合的实例和时间。这意味着,或许, end()值可能会发生变化,但old_end == end()比较仍应产生真实(编辑:虽然在阅读了j_random_hacker的评论后,我怀疑这一段本身的评估结果是 true ;-),而不是普遍的 - 请参阅下面的讨论)

我也怀疑你可以在std::map<int,Node>::iterator类中使用Node,因为类型不完整,但是(不确定)。

此外,由于您的节点是唯一编号的,因此您可以使用int来键入它们并为无效保留一些值。

答案 3 :(得分:1)

假设(1)用红黑树(2)实现的地图,你在“多次插入/删除后”使用相同的实例 - 回答“是”。

相对的implmentation我可以告诉我所知道的stl的所有化身都使用树算法。

答案 4 :(得分:1)

几点:

1)end()引用一个超过容器末尾的元素。插入或删除更改容器时它不会更改,因为它没有指向元素。

2)我想也许你可以改变你在Node中存储4个迭代器数组的想法,以使整个问题更有意义。你想要的是向Graph对象添加一个新的迭代器类型,它能够迭代单个节点的邻居。这个迭代器的实现需要访问map的成员,这可能会让你走上使Graph类扩展map集合的道路。如果Graph类是扩展的std :: map,那么语言会发生变化,您不再需要存储无效的迭代器,而只需要编写算法来确定谁是地图中的“下一个邻居”。 / p>

答案 5 :(得分:1)

(多)集和(多)映射中的迭代器在插入和删除时不会失效,因此将.end()与之前存储的.end()进行比较将始终产生真实

以GNU libstdc ++实现为例,其中.end()在maps中返回默认的初始化值Rb_tree_node

来自stl_tree.h:

  _M_initialize()
  {
    this->_M_header._M_color = _S_red;
    this->_M_header._M_parent = 0;
    this->_M_header._M_left = &this->_M_header;
    this->_M_header._M_right = &this->_M_header;
  }

答案 6 :(得分:1)

我认为很清楚:

  • end()返回一个遍及结尾的元素的迭代器。

  • 插入/删除不影响现有的迭代器,因此返回的值始终有效(除非您尝试删除传递的元素然后结束(但这会导致未定义的行为)。

  • 因此,当与使用operator ==的原始数据进行比较时,end()生成的任何新迭代器(将会有所不同)将返回true。

  • 使用赋值运算符生成的任何中间值=都有一个post条件,它们与operator ==相等,而operator ==对于迭代器是传递的。

所以是的,存储end()返回的迭代器是有效的(但是由于带有关联容器的保证,因此对于vector等无效)。

请记住,迭代器不一定是指针。它可能是容器的设计者已经定义了类的所有操作的对象。

答案 7 :(得分:0)

我认为这完全取决于使用什么类型的迭代器。

在向量中,end()是超过结束指针的那个,当插入和删除元素时,它会明显改变。

在另一种容器中,end()迭代器可能是一个特殊值,如NULL或默认构造元素。在这种情况下,它不会改变,因为它没有指向任何东西。而不是像指针一样的东西,end()只是一个值来比较。

我相信set和map迭代器是第二种,但我不知道有什么要求以这种方式实现它们。

答案 8 :(得分:0)

C ++ Standard声明迭代器应该保持有效。它是。标准明确规定在23.1.2 / 8中:

  

插入成员不会影响 迭代器的有效性,并且引用容器,并且擦除成员只会使迭代器和引用无效擦除元素。

在21.1 / 7中:

  

end()返回迭代器,这是容器的过去值

因此迭代器old_endnew_end将有效。这意味着我们可以获得--old_end(称之为it1)和--new_end(称之为it2)并且它将是结束值迭代器(来自定义的{{ {1}}返回),因为关联容器的迭代器是双向迭代器类别(根据23.1.2 / 6)并且根据{{1的定义操作(表75)。

现在end()应该等于--r,因为它给出了结束值,它只有一个(23.1.2 / 9)。然后从24.1.3开始: a == b暗示++ a == ++ b 的条件。 it1it2将提供++it1++it2迭代器(来自++ r操作表74的定义)。现在我们得到old_endnew_end 相等

答案 9 :(得分:0)

我最近有一个类似的问题,但我想知道调用end()来检索迭代器以进行比较是否可能有竞争条件。

根据标准,如果两个迭代器都可以取消引用并&*a == &*b ,或者两个迭代器都不能被解除引用,则它们被视为等效。查找粗体语句需要一段时间,并且在这里非常相关

因为除非它指向的元素已被删除,否则std::map::iterator无法失效,因此保证end返回两个迭代器,无论它们是什么状态。获得,将永远相互比较为真。