[llvm-branch-commits] [libcxx] release/22.x: [libc++] Introduce a escape hatch for the changed behavior of map and set search operations (#183190) (PR #183551)
via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Thu Feb 26 07:31:10 PST 2026
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-libcxx
Author: None (llvmbot)
<details>
<summary>Changes</summary>
Backport 5de92856bd6c238eb703202a8934f03480e80ee5
Requested by: @<!-- -->ldionne
---
Full diff: https://github.com/llvm/llvm-project/pull/183551.diff
5 Files Affected:
- (modified) libcxx/docs/ReleaseNotes/22.rst (+7)
- (modified) libcxx/include/__tree (+50)
- (modified) libcxx/include/__utility/lazy_synth_three_way_comparator.h (+14-2)
- (added) libcxx/test/libcxx/containers/associative/debug.non-strict-weak-ordering.pass.cpp (+130)
- (added) libcxx/test/libcxx/containers/associative/lower_upper_bound_non_strict_weak_order.pass.cpp (+151)
``````````diff
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);
+ }
+ }
+ }
+}
``````````
</details>
https://github.com/llvm/llvm-project/pull/183551
More information about the llvm-branch-commits
mailing list