[llvm-branch-commits] [libcxx] 9b197b5 - [libc++] Introduce a escape hatch for the changed behavior of map and set search operations (#183190)

Douglas Yung via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Thu Mar 5 09:24:51 PST 2026


Author: Louis Dionne
Date: 2026-03-05T17:24:34Z
New Revision: 9b197b5596bcd567da31c1bf6d3c1b096b2cead8

URL: https://github.com/llvm/llvm-project/commit/9b197b5596bcd567da31c1bf6d3c1b096b2cead8
DIFF: https://github.com/llvm/llvm-project/commit/9b197b5596bcd567da31c1bf6d3c1b096b2cead8.diff

LOG: [libc++] Introduce a escape hatch for the changed behavior of map and set search operations (#183190)

In #155245, we implemented an optimization to std::map and std::set
search operations. That optimization took advantage of something that is
guaranteed by the Standard, namely that the comparator provided to the
associative container is a valid strict weak ordering.

Sadly, some code in the wild did not satisfy this requirement, such as
Boost.ICL: boostorg/icl#51

Since this can have extremely tricky runtime consequences, this patch
introduces a temporary escape hatch for the LLVM 22 release that allows
reverting to the previous behavior. It also explicitly calls out the
change in the release notes, adds some regression tests and adds debug
mode support for catching some of these invalid predicates.

Fixes #183189

(cherry picked from commit 5de92856bd6c238eb703202a8934f03480e80ee5)

Added: 
    libcxx/test/libcxx/containers/associative/debug.non-strict-weak-ordering.pass.cpp
    libcxx/test/libcxx/containers/associative/lower_upper_bound_non_strict_weak_order.pass.cpp

Modified: 
    libcxx/docs/ReleaseNotes/22.rst
    libcxx/include/__tree
    libcxx/include/__utility/lazy_synth_three_way_comparator.h

Removed: 
    


################################################################################
diff  --git a/libcxx/docs/ReleaseNotes/22.rst b/libcxx/docs/ReleaseNotes/22.rst
index 59990d1aa5feb..9f9927095a61f 100644
--- a/libcxx/docs/ReleaseNotes/22.rst
+++ b/libcxx/docs/ReleaseNotes/22.rst
@@ -117,6 +117,13 @@ Potentially breaking changes
   first element being returned from ``find`` will be broken, and ``lower_bound`` or ``equal_range`` should be used
   instead.
 
+- The algorithms for ``std::{map,set}`` ``lower_bound`` and ``upper_bound`` operations were modified such that their
+  result changed for comparators that are not a strict weak order. Being a strict weak order was always a requirement
+  of the Standard and still is, however in this release libc++ changed the behavior of ``std::{map,set}`` for such
+  comparators. Since this may be tricky to work around in some cases, an escape hatch is provided in this release:
+  defining ``_LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUND`` will revert to the historical implementation of
+  these operations. That escape hatch will be removed in LLVM 23.
+
 - The ABI flag ``_LIBCPP_ABI_NO_REVERSE_ITERATOR_SECOND_MEMBER`` has been split off from
   ``_LIBCPP_ABI_NO_ITERATOR_BASES``. If you are using this flag and care about ABI stability, you should set
   ``_LIBCPP_ABI_NO_REVERSE_ITERATOR_SECOND_MEMBER`` as well.

diff  --git a/libcxx/include/__tree b/libcxx/include/__tree
index 84711057be409..eb17f7d36936c 100644
--- a/libcxx/include/__tree
+++ b/libcxx/include/__tree
@@ -1247,24 +1247,74 @@ public:
     return __result;
   }
 
+  // Compatibility escape hatch for comparators that are not strict weak orderings. This
+  // can be removed for the LLVM 23 release.
+#if defined(_LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUND)
+  template <class _Key>
+  _LIBCPP_HIDE_FROM_ABI __end_node_pointer __lower_bound_unique_compat_impl(const _Key& __v) const {
+    auto __rt     = __root();
+    auto __result = __end_node();
+    while (__rt != nullptr) {
+      if (!value_comp()(__rt->__get_value(), __v)) {
+        __result = std::__static_fancy_pointer_cast<__end_node_pointer>(__rt);
+        __rt     = std::__static_fancy_pointer_cast<__node_pointer>(__rt->__left_);
+      } else {
+        __rt = std::__static_fancy_pointer_cast<__node_pointer>(__rt->__right_);
+      }
+    }
+    return __result;
+  }
+
+  template <class _Key>
+  _LIBCPP_HIDE_FROM_ABI __end_node_pointer __upper_bound_unique_compat_impl(const _Key& __v) const {
+    auto __rt     = __root();
+    auto __result = __end_node();
+    while (__rt != nullptr) {
+      if (value_comp()(__v, __rt->__get_value())) {
+        __result = std::__static_fancy_pointer_cast<__end_node_pointer>(__rt);
+        __rt     = std::__static_fancy_pointer_cast<__node_pointer>(__rt->__left_);
+      } else {
+        __rt = std::__static_fancy_pointer_cast<__node_pointer>(__rt->__right_);
+      }
+    }
+    return __result;
+  }
+#endif // _LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUND
+
   template <class _Key>
   _LIBCPP_HIDE_FROM_ABI iterator __lower_bound_unique(const _Key& __v) {
+#if defined(_LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUND)
+    return iterator(__lower_bound_unique_compat_impl(__v));
+#else
     return iterator(__lower_upper_bound_unique_impl<true>(__v));
+#endif
   }
 
   template <class _Key>
   _LIBCPP_HIDE_FROM_ABI const_iterator __lower_bound_unique(const _Key& __v) const {
+#if defined(_LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUND)
+    return const_iterator(__lower_bound_unique_compat_impl(__v));
+#else
     return const_iterator(__lower_upper_bound_unique_impl<true>(__v));
+#endif
   }
 
   template <class _Key>
   _LIBCPP_HIDE_FROM_ABI iterator __upper_bound_unique(const _Key& __v) {
+#if defined(_LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUND)
+    return iterator(__upper_bound_unique_compat_impl(__v));
+#else
     return iterator(__lower_upper_bound_unique_impl<false>(__v));
+#endif
   }
 
   template <class _Key>
   _LIBCPP_HIDE_FROM_ABI const_iterator __upper_bound_unique(const _Key& __v) const {
+#if defined(_LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUND)
+    return iterator(__upper_bound_unique_compat_impl(__v));
+#else
     return iterator(__lower_upper_bound_unique_impl<false>(__v));
+#endif
   }
 
 private:

diff  --git a/libcxx/include/__utility/lazy_synth_three_way_comparator.h b/libcxx/include/__utility/lazy_synth_three_way_comparator.h
index 8c78742ccb4e3..906166bd2a61a 100644
--- a/libcxx/include/__utility/lazy_synth_three_way_comparator.h
+++ b/libcxx/include/__utility/lazy_synth_three_way_comparator.h
@@ -9,6 +9,7 @@
 #ifndef _LIBCPP___UTILITY_LAZY_SYNTH_THREE_WAY_COMPARATOR_H
 #define _LIBCPP___UTILITY_LAZY_SYNTH_THREE_WAY_COMPARATOR_H
 
+#include <__assert>
 #include <__config>
 #include <__type_traits/conjunction.h>
 #include <__type_traits/desugars_to.h>
@@ -40,8 +41,19 @@ struct __lazy_compare_result {
                         _LIBCPP_CTOR_LIFETIMEBOUND const _RHS& __rhs)
       : __comp_(__comp), __lhs_(__lhs), __rhs_(__rhs) {}
 
-  _LIBCPP_HIDE_FROM_ABI bool __less() const { return __comp_(__lhs_, __rhs_); }
-  _LIBCPP_HIDE_FROM_ABI bool __greater() const { return __comp_(__rhs_, __lhs_); }
+  _LIBCPP_HIDE_FROM_ABI bool __less() const {
+    bool __result = __comp_(__lhs_, __rhs_);
+    _LIBCPP_ASSERT_SEMANTIC_REQUIREMENT(__result ? !static_cast<bool>(__comp_(__rhs_, __lhs_)) : true,
+                                        "Comparator does not induce a strict weak ordering");
+    return __result;
+  }
+
+  _LIBCPP_HIDE_FROM_ABI bool __greater() const {
+    bool __result = __comp_(__rhs_, __lhs_);
+    _LIBCPP_ASSERT_SEMANTIC_REQUIREMENT(__result ? !static_cast<bool>(__comp_(__lhs_, __rhs_)) : true,
+                                        "Comparator does not induce a strict weak ordering");
+    return __result;
+  }
 };
 
 // This class provides three way comparison between _LHS and _RHS as efficiently as possible. This can be specialized if

diff  --git a/libcxx/test/libcxx/containers/associative/debug.non-strict-weak-ordering.pass.cpp b/libcxx/test/libcxx/containers/associative/debug.non-strict-weak-ordering.pass.cpp
new file mode 100644
index 0000000000000..00a3a0730545d
--- /dev/null
+++ b/libcxx/test/libcxx/containers/associative/debug.non-strict-weak-ordering.pass.cpp
@@ -0,0 +1,130 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// <set>
+// <map>
+
+// This test ensures that libc++ detects when std::set or std::map are used with a
+// predicate that is not a strict weak ordering when the debug mode is enabled.
+
+// REQUIRES: libcpp-hardening-mode=debug
+// UNSUPPORTED: c++03, c++11, c++14
+
+#include <map>
+#include <set>
+#include <utility>
+
+#include "check_assertion.h"
+#include "test_macros.h"
+
+struct InvalidLess {
+  bool operator()(int a, int b) const {
+    if (b == 0)
+      return true;
+    return static_cast<bool>(a % b);
+  }
+};
+
+int main() {
+  // std::set
+  {
+    // find
+    {
+      std::set<int, InvalidLess> s = {1, 2, 3, 4};
+      TEST_LIBCPP_ASSERT_FAILURE(s.find(4), "Comparator does not induce a strict weak ordering");
+      TEST_LIBCPP_ASSERT_FAILURE(std::as_const(s).find(4), "Comparator does not induce a strict weak ordering");
+    }
+
+    // upper_bound
+    {
+      std::set<int, InvalidLess> s = {1, 2, 3, 4};
+      TEST_LIBCPP_ASSERT_FAILURE(s.upper_bound(4), "Comparator does not induce a strict weak ordering");
+      TEST_LIBCPP_ASSERT_FAILURE(std::as_const(s).upper_bound(4), "Comparator does not induce a strict weak ordering");
+    }
+
+    // lower_bound
+    {
+      std::set<int, InvalidLess> s = {1, 2, 3, 4};
+      TEST_LIBCPP_ASSERT_FAILURE(s.lower_bound(4), "Comparator does not induce a strict weak ordering");
+      TEST_LIBCPP_ASSERT_FAILURE(std::as_const(s).lower_bound(4), "Comparator does not induce a strict weak ordering");
+    }
+
+    // equal_range
+    {
+      std::set<int, InvalidLess> s = {1, 2, 3, 4};
+      TEST_LIBCPP_ASSERT_FAILURE(s.equal_range(4), "Comparator does not induce a strict weak ordering");
+      TEST_LIBCPP_ASSERT_FAILURE(std::as_const(s).equal_range(4), "Comparator does not induce a strict weak ordering");
+    }
+
+    // count
+    {
+      std::set<int, InvalidLess> s = {1, 2, 3, 4};
+      TEST_LIBCPP_ASSERT_FAILURE(s.count(4), "Comparator does not induce a strict weak ordering");
+      TEST_LIBCPP_ASSERT_FAILURE(std::as_const(s).count(4), "Comparator does not induce a strict weak ordering");
+    }
+
+    // contains
+#if TEST_STD_VER >= 20
+    {
+      std::set<int, InvalidLess> s = {1, 2, 3, 4};
+      TEST_LIBCPP_ASSERT_FAILURE(s.contains(4), "Comparator does not induce a strict weak ordering");
+      TEST_LIBCPP_ASSERT_FAILURE(std::as_const(s).contains(4), "Comparator does not induce a strict weak ordering");
+    }
+#endif
+  }
+
+  // std::map
+  {
+    using X   = int;
+    X const x = 99;
+
+    // find
+    {
+      std::map<int, X, InvalidLess> s = {{1, x}, {2, x}, {3, x}, {4, x}};
+      TEST_LIBCPP_ASSERT_FAILURE(s.find(4), "Comparator does not induce a strict weak ordering");
+      TEST_LIBCPP_ASSERT_FAILURE(std::as_const(s).find(4), "Comparator does not induce a strict weak ordering");
+    }
+
+    // upper_bound
+    {
+      std::map<int, X, InvalidLess> s = {{1, x}, {2, x}, {3, x}, {4, x}};
+      TEST_LIBCPP_ASSERT_FAILURE(s.upper_bound(4), "Comparator does not induce a strict weak ordering");
+      TEST_LIBCPP_ASSERT_FAILURE(std::as_const(s).upper_bound(4), "Comparator does not induce a strict weak ordering");
+    }
+
+    // lower_bound
+    {
+      std::map<int, X, InvalidLess> s = {{1, x}, {2, x}, {3, x}, {4, x}};
+      TEST_LIBCPP_ASSERT_FAILURE(s.lower_bound(4), "Comparator does not induce a strict weak ordering");
+      TEST_LIBCPP_ASSERT_FAILURE(std::as_const(s).lower_bound(4), "Comparator does not induce a strict weak ordering");
+    }
+
+    // equal_range
+    {
+      std::map<int, X, InvalidLess> s = {{1, x}, {2, x}, {3, x}, {4, x}};
+      TEST_LIBCPP_ASSERT_FAILURE(s.equal_range(4), "Comparator does not induce a strict weak ordering");
+      TEST_LIBCPP_ASSERT_FAILURE(std::as_const(s).equal_range(4), "Comparator does not induce a strict weak ordering");
+    }
+
+    // count
+    {
+      std::map<int, X, InvalidLess> s = {{1, x}, {2, x}, {3, x}, {4, x}};
+      TEST_LIBCPP_ASSERT_FAILURE(s.count(4), "Comparator does not induce a strict weak ordering");
+      TEST_LIBCPP_ASSERT_FAILURE(std::as_const(s).count(4), "Comparator does not induce a strict weak ordering");
+    }
+
+    // contains
+#if TEST_STD_VER >= 20
+    {
+      std::map<int, X, InvalidLess> s = {{1, x}, {2, x}, {3, x}, {4, x}};
+      TEST_LIBCPP_ASSERT_FAILURE(s.contains(4), "Comparator does not induce a strict weak ordering");
+      TEST_LIBCPP_ASSERT_FAILURE(std::as_const(s).contains(4), "Comparator does not induce a strict weak ordering");
+    }
+#endif
+  }
+}

diff  --git a/libcxx/test/libcxx/containers/associative/lower_upper_bound_non_strict_weak_order.pass.cpp b/libcxx/test/libcxx/containers/associative/lower_upper_bound_non_strict_weak_order.pass.cpp
new file mode 100644
index 0000000000000..a3e2f3bb631b3
--- /dev/null
+++ b/libcxx/test/libcxx/containers/associative/lower_upper_bound_non_strict_weak_order.pass.cpp
@@ -0,0 +1,151 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// <set>
+// <map>
+
+// This test ensures that libc++ maintains its historical behavior when std::set
+// or std::map are used with a comparator that isn't quite a strict weak ordering
+// when _LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUND is defined.
+//
+// This escape hatch is only supported as a temporary means to give a bit more time
+// for codebases to fix incorrect usages of associative containers.
+
+// This precise test reproduces a comparator similar to boost::icl::exclusive_less
+// for discrete right-open intervals: it orders disjoint intervals but treats
+// overlapping intervals as equivalent, which is incorrect but was relied upon.
+
+// ADDITIONAL_COMPILE_FLAGS: -D_LIBCPP_ENABLE_LEGACY_TREE_LOWER_UPPER_BOUND
+// UNSUPPORTED: c++03, c++11, c++14
+
+#include <cassert>
+#include <map>
+#include <set>
+#include <utility>
+
+struct Interval {
+  int lower;
+  int upper;
+};
+
+struct ExclusiveLess {
+  bool operator()(const Interval& lhs, const Interval& rhs) const { return lhs.upper <= rhs.lower; }
+};
+
+Interval right_open(int lo, int hi) { return Interval{lo, hi}; }
+
+int main() {
+  // std::set
+  {
+    // upper_bound
+    {
+      std::set<Interval, ExclusiveLess> intervals = {
+          right_open(0, 2),
+          right_open(3, 5),
+          right_open(6, 9),
+          right_open(10, 12),
+      };
+      // non-const
+      {
+        auto it = intervals.upper_bound(right_open(4, 7));
+        assert(it != intervals.end());
+        assert(it->lower == 10);
+        assert(it->upper == 12);
+      }
+
+      // const
+      {
+        auto it = std::as_const(intervals).upper_bound(right_open(4, 7));
+        assert(it != intervals.end());
+        assert(it->lower == 10);
+        assert(it->upper == 12);
+      }
+    }
+
+    // lower_bound
+    {
+      std::set<Interval, ExclusiveLess> intervals = {
+          right_open(0, 2),
+          right_open(3, 5),
+          right_open(6, 9),
+          right_open(10, 12),
+      };
+      // non-const
+      {
+        auto it = intervals.lower_bound(right_open(1, 4));
+        assert(it != intervals.end());
+        assert(it->lower == 0);
+        assert(it->upper == 2);
+      }
+
+      // const
+      {
+        auto it = std::as_const(intervals).lower_bound(right_open(1, 4));
+        assert(it != intervals.end());
+        assert(it->lower == 0);
+        assert(it->upper == 2);
+      }
+    }
+  }
+
+  // std::map
+  {
+    using X   = int;
+    X const x = 99;
+
+    // upper_bound
+    {
+      std::map<Interval, X, ExclusiveLess> intervals = {
+          {right_open(0, 2), x},
+          {right_open(3, 5), x},
+          {right_open(6, 9), x},
+          {right_open(10, 12), x},
+      };
+      // non-const
+      {
+        auto it = intervals.upper_bound(right_open(4, 7));
+        assert(it != intervals.end());
+        assert(it->first.lower == 10);
+        assert(it->first.upper == 12);
+      }
+
+      // const
+      {
+        auto it = std::as_const(intervals).upper_bound(right_open(4, 7));
+        assert(it != intervals.end());
+        assert(it->first.lower == 10);
+        assert(it->first.upper == 12);
+      }
+    }
+
+    // lower_bound
+    {
+      std::map<Interval, X, ExclusiveLess> intervals = {
+          {right_open(0, 2), x},
+          {right_open(3, 5), x},
+          {right_open(6, 9), x},
+          {right_open(10, 12), x},
+      };
+      // non-const
+      {
+        auto it = intervals.lower_bound(right_open(1, 4));
+        assert(it != intervals.end());
+        assert(it->first.lower == 0);
+        assert(it->first.upper == 2);
+      }
+
+      // const
+      {
+        auto it = std::as_const(intervals).lower_bound(right_open(1, 4));
+        assert(it != intervals.end());
+        assert(it->first.lower == 0);
+        assert(it->first.upper == 2);
+      }
+    }
+  }
+}


        


More information about the llvm-branch-commits mailing list