Skip to content

[libc++] Add ranges::fold_left_first and ranges::fold_left_first_with_iter #121558

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

JCGoran
Copy link

@JCGoran JCGoran commented Jan 3, 2025

Implementation mostly borrowed from #75259.

Copy link

github-actions bot commented Jan 3, 2025

Thank you for submitting a Pull Request (PR) to the LLVM Project!

This PR will be automatically labeled and the relevant teams will be notified.

If you wish to, you can add reviewers by using the "Reviewers" section on this page.

If this is not working for you, it is probably because you do not have write permissions for the repository. In which case you can instead tag reviewers by name in a comment by using @ followed by their GitHub username.

If you have received no comments on your PR for a week, you can request a review by "ping"ing the PR by adding a comment “Ping”. The common courtesy "ping" rate is once a week. Please remember that you are asking for valuable time from other developers.

If you have further questions, they may be answered by the LLVM GitHub User Guide.

You can also ask questions in a comment on this PR, on the LLVM Discord or on the forums.

Copy link
Contributor

@frederick-vs-ja frederick-vs-ja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Mostly LGTM!

Copy link

github-actions bot commented Jan 3, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@JCGoran JCGoran marked this pull request as ready for review January 7, 2025 16:33
@JCGoran JCGoran requested a review from a team as a code owner January 7, 2025 16:33
@llvmbot llvmbot added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label Jan 7, 2025
@llvmbot
Copy link
Member

llvmbot commented Jan 7, 2025

@llvm/pr-subscribers-libcxx

Author: None (JCGoran)

Changes

Implementation mostly borrowed from #75259.
Probably needs more tests, hence just a draft for now.


Full diff: https://github.com/llvm/llvm-project/pull/121558.diff

5 Files Affected:

  • (modified) libcxx/include/__algorithm/ranges_fold.h (+53)
  • (modified) libcxx/include/algorithm (+21)
  • (modified) libcxx/modules/std/algorithm.inc (+3-4)
  • (modified) libcxx/test/libcxx/diagnostics/algorithm.nodiscard.verify.cpp (+4)
  • (modified) libcxx/test/std/algorithms/alg.nonmodifying/alg.fold/left_folds.pass.cpp (+58)
diff --git a/libcxx/include/__algorithm/ranges_fold.h b/libcxx/include/__algorithm/ranges_fold.h
index d2c39213985044..b94fb59f6090a9 100644
--- a/libcxx/include/__algorithm/ranges_fold.h
+++ b/libcxx/include/__algorithm/ranges_fold.h
@@ -28,6 +28,7 @@
 #include <__type_traits/invoke.h>
 #include <__utility/forward.h>
 #include <__utility/move.h>
+#include <optional>
 
 #if !defined(_LIBCPP_HAS_NO_PRAGMA_SYSTEM_HEADER)
 #  pragma GCC system_header
@@ -62,6 +63,9 @@ struct in_value_result {
 template <class _Ip, class _Tp>
 using fold_left_with_iter_result = in_value_result<_Ip, _Tp>;
 
+template <class _Ip, class _Tp>
+using fold_left_first_with_iter_result = in_value_result<_Ip, _Tp>;
+
 template <class _Fp, class _Tp, class _Ip, class _Rp, class _Up = decay_t<_Rp>>
 concept __indirectly_binary_left_foldable_impl =
     convertible_to<_Rp, _Up> &&                    //
@@ -118,6 +122,55 @@ struct __fold_left {
 };
 
 inline constexpr auto fold_left = __fold_left();
+
+struct __fold_left_first_with_iter {
+  template <input_iterator _Ip, sentinel_for<_Ip> _Sp, __indirectly_binary_left_foldable<iter_value_t<_Ip>, _Ip> _Fp>
+    requires constructible_from<iter_value_t<_Ip>, iter_reference_t<_Ip>>
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI static constexpr auto operator()(_Ip __first, _Sp __last, _Fp __f) {
+    using _Up = decltype(fold_left(std::move(__first), __last, iter_value_t<_Ip>(*__first), __f));
+
+    // case of empty range
+    if (__first == __last) {
+      return fold_left_first_with_iter_result<_Ip, optional<_Up>>{std::move(__first), optional<_Up>()};
+    }
+
+    _Up __result(*__first);
+    for (++__first; __first != __last; ++__first) {
+      __result = std::invoke(__f, std::move(__result), *__first);
+    }
+
+    return fold_left_first_with_iter_result<_Ip, optional<_Up>>{std::move(__first), optional<_Up>(std::move(__result))};
+  }
+
+  template <input_range _Rp, __indirectly_binary_left_foldable<range_value_t<_Rp>, iterator_t<_Rp>> _Fp>
+    requires constructible_from<range_value_t<_Rp>, range_reference_t<_Rp>>
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI static constexpr auto operator()(_Rp&& __r, _Fp __f) {
+    auto __result = operator()(ranges::begin(__r), ranges::end(__r), std::ref(__f));
+
+    using _Up = decltype(fold_left(ranges::begin(__r), ranges::end(__r), range_value_t<_Rp>(*ranges::begin(__r)), __f));
+    return fold_left_first_with_iter_result<borrowed_iterator_t<_Rp>, optional<_Up>>{
+        std::move(__result.in), std::move(__result.value)};
+  }
+};
+
+inline constexpr auto fold_left_first_with_iter = __fold_left_first_with_iter();
+
+struct __fold_left_first {
+  template <input_iterator _Ip, sentinel_for<_Ip> _Sp, __indirectly_binary_left_foldable<iter_value_t<_Ip>, _Ip> _Fp>
+    requires constructible_from<iter_value_t<_Ip>, iter_reference_t<_Ip>>
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI static constexpr auto operator()(_Ip __first, _Sp __last, _Fp __f) {
+    return fold_left_first_with_iter(std::move(__first), std::move(__last), std::ref(__f)).value;
+  }
+
+  template <input_range _Rp, __indirectly_binary_left_foldable<range_value_t<_Rp>, iterator_t<_Rp>> _Fp>
+    requires constructible_from<range_value_t<_Rp>, range_reference_t<_Rp>>
+  [[nodiscard]] _LIBCPP_HIDE_FROM_ABI static constexpr auto operator()(_Rp&& __r, _Fp __f) {
+    return fold_left_first_with_iter(ranges::begin(__r), ranges::end(__r), std::ref(__f)).value;
+  }
+};
+
+inline constexpr auto fold_left_first = __fold_left_first();
+
 } // namespace ranges
 
 #endif // _LIBCPP_STD_VER >= 23
diff --git a/libcxx/include/algorithm b/libcxx/include/algorithm
index e593ae26ed6e24..5592387e454950 100644
--- a/libcxx/include/algorithm
+++ b/libcxx/include/algorithm
@@ -938,6 +938,15 @@ namespace ranges {
   template<input_range R, class T, indirectly-binary-left-foldable<T, iterator_t<R>> F>
     constexpr auto fold_left(R&& r, T init, F f);                                                   // since C++23
 
+  template<input_iterator I, sentinel_for<I> S,
+           indirectly-binary-left-foldable<T, I> F>
+    requires constructible_from<iter_value_t<I>, iter_reference_t<I>>
+    constexpr see below fold_left_first(I first, S last, F f);                                      // since C++23
+
+  template<input_range R, indirectly-binary-left-foldable<T, iterator_t<R>> F>
+    requires constructible_from<range_value_t<R>, range_reference_t<R>>
+    constexpr see below fold_left_first(R&& r, F f);                                                // since C++23
+
   template<class I, class T>
     using fold_left_with_iter_result = in_value_result<I, T>;                                       // since C++23
 
@@ -948,6 +957,18 @@ namespace ranges {
   template<input_range R, class T, indirectly-binary-left-foldable<T, iterator_t<R>> F>
     constexpr see below fold_left_with_iter(R&& r, T init, F f);                                    // since C++23
 
+  template<class I, class T>
+    using fold_left_first_with_iter_result = in_value_result<I, T>;                                 // since C++23
+
+  template<input_iterator I, sentinel_for<I> S,
+           indirectly-binary-left-foldable<T, I> F>
+    requires constructible_from<iter_value_t<I>, iter_reference_t<I>>
+    constexpr see below fold_left_first_with_iter(I first, S last, F f);                            // since C++23
+
+  template<input_range R, indirectly-binary-left-foldable<T, iterator_t<R>> F>
+    requires constructible_from<range_value_t<R>, range_reference_t<R>>
+    constexpr see below fold_left_first_with_iter(R&& r, F f);                                      // since C++23
+
   template<forward_iterator I1, sentinel_for<I1> S1, forward_iterator I2, sentinel_for<I2> S2,
            class Pred = ranges::equal_to, class Proj1 = identity, class Proj2 = identity>
     requires indirectly_comparable<I1, I2, Pred, Proj1, Proj2>
diff --git a/libcxx/modules/std/algorithm.inc b/libcxx/modules/std/algorithm.inc
index 3c2139cd64ee49..20a3aea4fa8dab 100644
--- a/libcxx/modules/std/algorithm.inc
+++ b/libcxx/modules/std/algorithm.inc
@@ -162,15 +162,14 @@ export namespace std {
 
     // [alg.fold], fold
     using std::ranges::fold_left;
+    using std::ranges::fold_left_first;
+    using std::ranges::fold_left_first_with_iter;
+    using std::ranges::fold_left_first_with_iter_result;
     using std::ranges::fold_left_with_iter;
     using std::ranges::fold_left_with_iter_result;
 #  if 0
-    using std::ranges::fold_left_first;
     using std::ranges::fold_right;
     using std::ranges::fold_right_last;
-    using std::ranges::fold_left_with_iter;
-    using std::ranges::fold_left_first_with_iter;
-    using std::ranges::fold_left_first_with_iter;
 #  endif
 #endif // _LIBCPP_STD_VER >= 23
   }    // namespace ranges
diff --git a/libcxx/test/libcxx/diagnostics/algorithm.nodiscard.verify.cpp b/libcxx/test/libcxx/diagnostics/algorithm.nodiscard.verify.cpp
index 14febc12a8a2d9..f4f14e513308ba 100644
--- a/libcxx/test/libcxx/diagnostics/algorithm.nodiscard.verify.cpp
+++ b/libcxx/test/libcxx/diagnostics/algorithm.nodiscard.verify.cpp
@@ -392,5 +392,9 @@ void test() {
   // expected-warning@-1{{ignoring return value of function declared with 'nodiscard' attribute}}
   std::ranges::fold_left_with_iter(iter, iter, 0, std::plus());
   // expected-warning@-1{{ignoring return value of function declared with 'nodiscard' attribute}}
+  std::ranges::fold_left_first_with_iter(range, std::plus());
+  // expected-warning@-1{{ignoring return value of function declared with 'nodiscard' attribute}}
+  std::ranges::fold_left_first_with_iter(iter, iter, std::plus());
+  // expected-warning@-1{{ignoring return value of function declared with 'nodiscard' attribute}}
 #endif
 }
diff --git a/libcxx/test/std/algorithms/alg.nonmodifying/alg.fold/left_folds.pass.cpp b/libcxx/test/std/algorithms/alg.nonmodifying/alg.fold/left_folds.pass.cpp
index 4987ca9cac4ae0..c5b75a2e4ca3c2 100644
--- a/libcxx/test/std/algorithms/alg.nonmodifying/alg.fold/left_folds.pass.cpp
+++ b/libcxx/test/std/algorithms/alg.nonmodifying/alg.fold/left_folds.pass.cpp
@@ -39,6 +39,7 @@
 #include <string_view>
 #include <string>
 #include <vector>
+#include <optional>
 
 #include "test_macros.h"
 #include "test_range.h"
@@ -50,6 +51,8 @@
 #endif
 
 using std::ranges::fold_left;
+using std::ranges::fold_left_first;
+using std::ranges::fold_left_first_with_iter;
 using std::ranges::fold_left_with_iter;
 
 template <class Result, class Range, class T>
@@ -105,6 +108,48 @@ constexpr void check_iterator(R& r, T const& init, F f, Expected const& expected
   }
 }
 
+template <std::ranges::input_range R, class F, std::equality_comparable Expected>
+  requires std::copyable<R>
+constexpr void check_iterator(R& r, F f, std::optional<Expected> const& expected) {
+  {
+    is_in_value_result<R, std::optional<Expected>> decltype(auto) result =
+        fold_left_first_with_iter(r.begin(), r.end(), f);
+    assert(result.in == r.end());
+    assert(result.value == expected);
+  }
+
+  {
+    auto telemetry = invocable_telemetry();
+    auto f2        = invocable_with_telemetry(f, telemetry);
+    is_in_value_result<R, std::optional<Expected>> decltype(auto) result =
+        fold_left_first_with_iter(r.begin(), r.end(), f2);
+    assert(result.in == r.end());
+    assert(result.value == expected);
+    if (result.value.has_value()) {
+      assert(telemetry.invocations == std::ranges::distance(r) - 1);
+      assert(telemetry.moves == 0);
+      assert(telemetry.copies == 1);
+    }
+  }
+
+  {
+    std::same_as<std::optional<Expected>> decltype(auto) result = fold_left_first(r.begin(), r.end(), f);
+    assert(result == expected);
+  }
+
+  {
+    auto telemetry                                              = invocable_telemetry();
+    auto f2                                                     = invocable_with_telemetry(f, telemetry);
+    std::same_as<std::optional<Expected>> decltype(auto) result = fold_left_first(r.begin(), r.end(), f2);
+    assert(result == expected);
+    if (result.has_value()) {
+      assert(telemetry.invocations == std::ranges::distance(r) - 1);
+      assert(telemetry.moves == 0);
+      assert(telemetry.copies == 1);
+    }
+  }
+}
+
 template <std::ranges::input_range R, class T, class F, std::equality_comparable Expected>
   requires std::copyable<R>
 constexpr void check_lvalue_range(R& r, T const& init, F f, Expected const& expected) {
@@ -186,19 +231,29 @@ constexpr void check(R r, T const& init, F f, Expected const& expected) {
   check_rvalue_range(r, init, f, expected);
 }
 
+template <std::ranges::input_range R, class F, std::equality_comparable Expected>
+  requires std::copyable<R>
+constexpr void check(R r, F f, std::optional<Expected> const& expected) {
+  check_iterator(r, f, expected);
+}
+
 constexpr void empty_range_test_case() {
   auto const data = std::vector<int>{};
   check(data, 100, std::plus(), 100);
   check(data, -100, std::multiplies(), -100);
+  check(data, std::plus(), std::optional<int>());
 
   check(data | std::views::take_while([](auto) { return false; }), 1.23, std::plus(), 1.23);
   check(data, Integer(52), &Integer::plus, Integer(52));
+  check(data | std::views::take_while([](auto) { return false; }), std::plus(), std::optional<int>());
 }
 
 constexpr void common_range_test_case() {
   auto const data = std::vector<int>{1, 2, 3, 4};
   check(data, 0, std::plus(), triangular_sum(data));
   check(data, 1, std::multiplies(), factorial(data.back()));
+  check(data, std::plus(), std::optional(triangular_sum(data)));
+  check(data, std::multiplies(), std::optional(factorial(data.back())));
 
   auto multiply_with_prev = [n = 1](auto const x, auto const y) mutable {
     auto const result = x * y * n;
@@ -206,6 +261,7 @@ constexpr void common_range_test_case() {
     return static_cast<std::size_t>(result);
   };
   check(data, 1, multiply_with_prev, factorial(data.size()) * factorial(data.size() - 1));
+  check(data, multiply_with_prev, std::optional(factorial(data.size()) * factorial(data.size() - 1)));
 
   auto fib = [n = 1](auto x, auto) mutable {
     auto old_x = x;
@@ -237,6 +293,7 @@ constexpr void non_common_range_test_case() {
     auto data  = std::vector<std::string>{"five", "three", "two", "six", "one", "four"};
     auto range = data | std::views::transform(parse);
     check(range, 0, std::plus(), triangular_sum(range));
+    check(range, std::plus(), std::optional(triangular_sum(range)));
   }
 
   {
@@ -248,6 +305,7 @@ constexpr void non_common_range_test_case() {
     auto range =
         std::views::lazy_split(data, ' ') | std::views::transform(to_string_view) | std::views::transform(parse);
     check(range, 0, std::plus(), triangular_sum(range));
+    check(range, std::plus(), std::optional(triangular_sum(range)));
   }
 }
 

@JCGoran
Copy link
Author

JCGoran commented Feb 5, 2025

Ping

2 similar comments
@JCGoran
Copy link
Author

JCGoran commented Feb 15, 2025

Ping

@JCGoran
Copy link
Author

JCGoran commented Feb 25, 2025

Ping

@frederick-vs-ja
Copy link
Contributor

I think we're very near to landing this. Let's add notes to libcxx/docs/ReleaseNotes/21.rst.

Copy link
Contributor

@frederick-vs-ja frederick-vs-ja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tentatively approving. I pushed changes which added release notes to the documentation. @JCGoran Please confirm whether these notes are fine.

@frederick-vs-ja frederick-vs-ja changed the title [libc++] Add ranges::fold_left_first and ranges::fold_left_first_with_iter [libc++] Add ranges::fold_left_first and ranges::fold_left_first_with_iter Mar 19, 2025
Copy link
Member

@mordante mordante left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this. In general it look good.

Comment on lines 54 to 55
using std::ranges::fold_left_first;
using std::ranges::fold_left_first_with_iter;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We prefer not to do this.

Also we prefer to have one test per function overload.
For tests of a set of properties we sometimes use one test like for the nodiscard test.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I've split this file into four algorithms, each for one algorithm.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what should do next... Should we cover different overloads in different function call entries, or in different test files?

@mordante mordante self-assigned this Mar 19, 2025
Copy link
Member

@mordante mordante left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good after addressing my last review comment.
Please apply that comment to all tests in this patch.

I like to have a quick look at the final tests.

constexpr void common_range_test_case() {
auto const data = std::vector<int>{1, 2, 3, 4};
check(data, std::plus(), std::optional(triangular_sum(data)));
check(data, std::multiplies(), std::optional(factorial(data.back())));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I missed this in the previous review. But I feel these tests are way too clever.

Suggested change
check(data, std::multiplies(), std::optional(factorial(data.back())));
check(data, std::multiplies(), std::optional(24));

makes the test a lot easier to inspect. It also allows to test the multiplies in the transform test below

    auto data  = std::vector<std::string>{"five", "three", "two", "six", "one", "four"};
    auto range = data | std::views::transform(parse);
    check(range, std::plus(), std::optional(triangular_sum(range)));
    check(data, std::multiplies(), std::optional(720)); // <<< This is now tested.

@@ -82,14 +86,12 @@ struct __fold_left_with_iter {
[[nodiscard]] _LIBCPP_HIDE_FROM_ABI static constexpr auto operator()(_Ip __first, _Sp __last, _Tp __init, _Fp __f) {
using _Up = decay_t<invoke_result_t<_Fp&, _Tp, iter_reference_t<_Ip>>>;

if (__first == __last) {
if (__first == __last)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid unrelated changes.

@Zingam
Copy link
Contributor

Zingam commented Apr 2, 2025

@JCGoran IMO it makes sense to create a sub-issue in #105208 and use the GitHub syntax to associate the PR with it: https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants