From a8c1ac2adcbadb53227865024a9e77eddcad704f Mon Sep 17 00:00:00 2001 From: Ben Deane Date: Wed, 18 Mar 2026 14:10:46 -0600 Subject: [PATCH] :art: Make `apply` work with types that implement the tuple protocol Problem: - `std::apply` works with `std::array`. `stdx::apply` does not. Solution: - Make `stdx::apply` work with types that implement the tuple protocol. Notes: - The standard uses the term *tuple-like* to mean specifically a specialization of `std::tuple`, `std::pair`, `std::array`, `std::complex` or `std::ranges::subrange`. (https://eel.is/c++draft/tuple.like) - The non-standard term of art "tuple protocol" refers to types that implement: - `tuple_size` - `tuple_element` - `get` Which *tuple-like* type do, but which is a more general constraint. --- .github/workflows/asciidoctor-ghpages.yml | 4 +- include/stdx/tuple.hpp | 58 ++++++++++++++++------- include/stdx/tuple_algorithms.hpp | 39 +++++++++++---- test/indexed_tuple.cpp | 10 ++++ test/tuple.cpp | 24 ++++++++++ test/tuple_algorithms.cpp | 28 +++++++++++ 6 files changed, 137 insertions(+), 26 deletions(-) diff --git a/.github/workflows/asciidoctor-ghpages.yml b/.github/workflows/asciidoctor-ghpages.yml index 06e7f66a..e6f84236 100644 --- a/.github/workflows/asciidoctor-ghpages.yml +++ b/.github/workflows/asciidoctor-ghpages.yml @@ -50,7 +50,7 @@ jobs: env: cache-name: cpm-cache-0 id: cpm-cache-restore - uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/cpm-cache key: ${{runner.os}}-${{env.cache-name}}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} @@ -64,7 +64,7 @@ jobs: env: cache-name: cpm-cache-0 if: steps.cpm-cache-restore.outputs.cache-hit != 'true' - uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 with: path: ~/cpm-cache key: ${{runner.os}}-${{env.cache-name}}-${{ hashFiles('**/CMakeLists.txt', 'cmake/**') }} diff --git a/include/stdx/tuple.hpp b/include/stdx/tuple.hpp index b3fccfac..52f8f778 100644 --- a/include/stdx/tuple.hpp +++ b/include/stdx/tuple.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -335,18 +336,6 @@ struct tuple_impl, index_function_list, Ts...> constexpr static auto size = std::integral_constant{}; - [[nodiscard]] constexpr static auto fill_inner_indices(index_pair *p) - -> index_pair * { - ((p++->inner = Is), ...); - return p; - } - [[nodiscard]] constexpr static auto - fill_outer_indices(index_pair *p, [[maybe_unused]] std::size_t n) - -> index_pair * { - ((p++->outer = (static_cast(Is), n)), ...); - return p; - } - private: template requires(... and std::equality_comparable_with) @@ -407,18 +396,56 @@ tuple_impl(Ts...) template constexpr auto tuple_size_v = T::size(); template constexpr auto tuple_size_v> = N; +template +constexpr auto tuple_size_v> = std::size_t{2}; template constexpr auto tuple_size_v> = std::size_t{N}; -template -using tuple_element_t = typename T::template element_t; - template concept tuple_comparable = requires { typename T::common_tuple_comparable; }; template concept tuplelike = requires { typename remove_cvref_t::is_tuple; }; +template struct tuple_element; + +template struct tuple_element { + using type = typename T::template element_t; +}; + +template +struct tuple_element> { + using type = T; +}; + +template +struct tuple_element> { + using type = nth_t; +}; + +template +using tuple_element_t = typename tuple_element::type; + +namespace detail { +template +concept has_vacuous_tuple_protocol = requires { + { + tuple_size_v> + } -> std::same_as; +}; +template +concept is_vacuous_tuple = tuple_size_v> == 0; +} // namespace detail + +template +concept has_tuple_protocol = + detail::has_vacuous_tuple_protocol and + (detail::is_vacuous_tuple or requires(T &t) { + { + get<0>(t) + } -> std::same_as> &>; + }); + template class tuple : public detail::tuple_impl, detail::index_function_list<>, Ts...> { @@ -524,6 +551,5 @@ class one_of : public detail::tuple_impl, } }; template one_of(Ts...) -> one_of; - } // namespace v1 } // namespace stdx diff --git a/include/stdx/tuple_algorithms.hpp b/include/stdx/tuple_algorithms.hpp index f719ff3d..30851b2b 100644 --- a/include/stdx/tuple_algorithms.hpp +++ b/include/stdx/tuple_algorithms.hpp @@ -14,7 +14,29 @@ namespace stdx { inline namespace v1 { -template constexpr auto apply(F &&f, Ts &&...ts) { +namespace detail { +template +[[nodiscard]] constexpr auto fill_inner_indices([[maybe_unused]] index_pair *p) + -> index_pair * { + return [&](std::index_sequence) { + ((p++->inner = Is), ...); + return p; + }(std::make_index_sequence>{}); +} + +template +[[nodiscard]] constexpr auto fill_outer_indices([[maybe_unused]] index_pair *p, + [[maybe_unused]] std::size_t n) + -> index_pair * { + return [&](std::index_sequence) { + ((p++->outer = (static_cast(Is), n)), ...); + return p; + }(std::make_index_sequence>{}); +} +} // namespace detail + +template +constexpr auto apply(F &&f, Ts &&...ts) { constexpr auto total_num_elements = (std::size_t{} + ... + stdx::tuple_size_v>); @@ -22,19 +44,19 @@ template constexpr auto apply(F &&f, Ts &&...ts) { [&]() -> std::array { std::array indices{}; [[maybe_unused]] auto p = indices.data(); - ((p = std::remove_cvref_t::fill_inner_indices(p)), ...); + ((p = detail::fill_inner_indices>(p)), ...); [[maybe_unused]] auto q = indices.data(); [[maybe_unused]] std::size_t n{}; - ((q = std::remove_cvref_t::fill_outer_indices(q, n++)), ...); + ((q = detail::fill_outer_indices>(q, n++)), + ...); return indices; }(); [[maybe_unused]] auto outer_tuple = stdx::tuple{std::forward(ts)...}; return [&](std::index_sequence) { - return std::forward(f)( - std::move(outer_tuple)[index] - [index]...); + return std::forward(f)(get( + get(std::move(outer_tuple)))...); }(std::make_index_sequence{}); } @@ -51,10 +73,11 @@ template [[nodiscard]] constexpr auto tuple_cat(Ts &&...ts) { [&]() -> std::array { std::array indices{}; auto p = indices.data(); - ((p = std::remove_cvref_t::fill_inner_indices(p)), ...); + ((p = detail::fill_inner_indices>(p)), ...); auto q = indices.data(); std::size_t n{}; - ((q = std::remove_cvref_t::fill_outer_indices(q, n++)), ...); + ((q = detail::fill_outer_indices>(q, n++)), + ...); return indices; }(); diff --git a/test/indexed_tuple.cpp b/test/indexed_tuple.cpp index f993ebbd..02656eae 100644 --- a/test/indexed_tuple.cpp +++ b/test/indexed_tuple.cpp @@ -17,6 +17,16 @@ template struct map_entry { template using key_for = typename T::key_t; } // namespace +TEST_CASE("indexed_tuple is tuplelike", "[tuple]") { + auto t = stdx::indexed_tuple{1, 2, 3}; + STATIC_CHECK(stdx::tuplelike); +} + +TEST_CASE("indexed_tuple has tuple protocol", "[tuple]") { + auto t = stdx::indexed_tuple{1, 2, 3}; + STATIC_CHECK(stdx::has_tuple_protocol); +} + TEST_CASE("make_indexed_tuple", "[indexed_tuple]") { STATIC_REQUIRE(stdx::make_indexed_tuple<>() == stdx::indexed_tuple{}); STATIC_REQUIRE(stdx::make_indexed_tuple<>(1, 2, 3) == diff --git a/test/tuple.cpp b/test/tuple.cpp index 944a6aef..9047510b 100644 --- a/test/tuple.cpp +++ b/test/tuple.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -44,6 +45,29 @@ TEST_CASE("multi element tuple", "[tuple]") { STATIC_CHECK(T::size() == 2); } +TEST_CASE("tuple is tuplelike", "[tuple]") { + auto t = stdx::tuple{1, 2, 3}; + STATIC_CHECK(stdx::tuplelike); + STATIC_CHECK(stdx::tuplelike>); +} + +TEST_CASE("tuple has tuple protocol", "[tuple]") { + auto t = stdx::tuple{1, 2, 3}; + STATIC_CHECK(stdx::has_tuple_protocol); + STATIC_CHECK(stdx::has_tuple_protocol>); +} + +TEST_CASE("std::array has tuple protocol", "[tuple_algorithms]") { + auto t = std::array{1, 2, 3}; + STATIC_CHECK(stdx::has_tuple_protocol); + STATIC_CHECK(stdx::has_tuple_protocol>); +} + +TEST_CASE("std::pair has tuple protocol", "[tuple_algorithms]") { + auto t = std::pair{1, 2}; + STATIC_CHECK(stdx::has_tuple_protocol); +} + namespace { template struct empty {}; } // namespace diff --git a/test/tuple_algorithms.cpp b/test/tuple_algorithms.cpp index b9cecef4..7f5523c1 100644 --- a/test/tuple_algorithms.cpp +++ b/test/tuple_algorithms.cpp @@ -116,6 +116,16 @@ TEST_CASE("apply preserves argument order", "[tuple_algorithms]") { CHECK(called == 1); } +TEST_CASE("apply works on std::array", "[tuple_algorithms]") { + STATIC_REQUIRE(stdx::apply([](auto... xs) { return (0 + ... + xs); }, + std::array{1, 2, 3, 4}) == 10); +} + +TEST_CASE("apply works on std::pair", "[tuple_algorithms]") { + STATIC_REQUIRE(stdx::apply([](auto... xs) { return (0 + ... + xs); }, + std::pair{1, 2}) == 3); +} + TEST_CASE("variadic apply", "[tuple_algorithms]") { STATIC_REQUIRE(stdx::apply([](auto... xs) { return (0 + ... + xs); }) == 0); STATIC_REQUIRE(stdx::apply([](auto... xs) { return (0 + ... + xs); }, @@ -161,6 +171,24 @@ TEST_CASE("variadic apply preserves argument order", "[tuple_algorithms]") { CHECK(called == 1); } +TEST_CASE("variadic apply works on std::array", "[tuple_algorithms]") { + STATIC_REQUIRE(stdx::apply([](auto... xs) { return (0 + ... + xs); }, + std::array{1, 2, 3, 4}, + std::array{1, 2, 3, 4, 5}) == 25); +} + +TEST_CASE("variadic apply works on std::pair", "[tuple_algorithms]") { + STATIC_REQUIRE(stdx::apply([](auto... xs) { return (0 + ... + xs); }, + std::pair{1, 2}, std::pair{3, 4}) == 10); +} + +TEST_CASE("variadic apply works on heterogeneous arguments", + "[tuple_algorithms]") { + STATIC_REQUIRE(stdx::apply([](auto... xs) { return (0 + ... + xs); }, + std::array{1, 2}, std::pair{3, 4}, + stdx::tuple{5, 6}) == 21); +} + TEST_CASE("join", "[tuple_algorithms]") { constexpr auto t = stdx::tuple{1, 2, 3}; STATIC_REQUIRE(t.join(std::plus{}) == 6);