(libstdc++) std::set
相关文件:
include/set头文件按顺序引入了include/bits/stl_tree.h和include/bits/stl_set.h。include/bits/stl_set.h定义了std::set,依赖了include/bits/stl_tree.h却没有明确包含它。include/bits/stl_tree.h定义了诸多类型,如未明确指出则默认下面提到的类型都是来自于这个头文件。src/c++98/tree.cc实现了红黑树用到的非模板算法。这个文件只有 400 多行。
std::set 将实现转发给 _Rb_tree 处理,而它又将实现转发给了 _Rb_tree_impl 处理。
_Rb_tree_impl 继承于:
_Node_allocator:是一个 typedef,实际类型是一个模板类,模板参数和_Rb_tree的参数有关。_Rb_tree_key_compare:是一个模板类,模板参数和_Rb_tree有关。_Rb_tree_header:非模板类。包含了_Rb_tree_node_base结构和_Rb_tree_node_count。前者是表示红黑树结点的聚合类,包含了 parent/left/right/color 四个重要成员;后者是结点计数。初始化时,parent/left/right 都被设置为 header 本身的地址,而 color 被设置为红色,可见这个 header 就是标准库红黑树中的 nil 哨兵。另外,left 和 right 还有最小结点和最大结点的含义。

以插入算法举例,由于比较算子在模板中,所以需要用模板方法找到应该插入的位置,这些都在头文件 include/bits/stl_tree.h 中实现。接着调用插入并平衡的函数,该函数只操作指针,不关心结点中包含的值,所以这个部分可以在源文件 src/c++98/tree.cc 中实现,从而减少了代码膨胀。
按照上面的分析,可以解释以下未定义行为(但是不建议这么写):
std::set<int> set;
auto it = set.end();
++it; // dead loop
由于 set.end() 返回了哨兵,初始化情况下三个指针都指向自己,所以按照自增算法会一直向右搜索,造成死循环。
std::set<int> set{1};
auto it = set.end();
++it;
assert(it != set.end());
++it;
assert(it == set.end());
由于哨兵的 right 指针指向最大元素,因此自增之后回到了最大元素的位置。再自增一次又回到了哨兵的位置。