diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index 615a8a2b..8b97a9b4 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -5,13 +5,15 @@ set(SPHINX_SOURCE ${CMAKE_CURRENT_SOURCE_DIR}) set(SPHINX_BUILD ${CMAKE_CURRENT_BINARY_DIR}/html) set(SPHINX_INDEX_FILE ${SPHINX_BUILD}/index.html) +file(GLOB_RECURSE SPHINX_RST_FILES ${CMAKE_CURRENT_SOURCE_DIR}/*.rst) + add_custom_command( OUTPUT ${SPHINX_INDEX_FILE} COMMAND ${SPHINX_EXECUTABLE} -b html ${SPHINX_SOURCE} ${SPHINX_BUILD} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} DEPENDS - ${CMAKE_CURRENT_SOURCE_DIR}/index.rst + ${SPHINX_RST_FILES} MAIN_DEPENDENCY ${CMAKE_CURRENT_SOURCE_DIR}/conf.py COMMENT "Generating documentation with Sphinx" ) diff --git a/docs/reference/adaptors.rst b/docs/reference/adaptors.rst index c4c09ac8..e261a7af 100644 --- a/docs/reference/adaptors.rst +++ b/docs/reference/adaptors.rst @@ -814,6 +814,93 @@ You can pass a reference to a sequence into an adaptor using :func:`flux::ref` o * - :concept:`const_iterable_sequence` - :var:`Seq` is const-iterable and :var:`Pred` is const-invocable +``filter_deref`` +^^^^^^^^^^^^^^^^ + +.. function:: + template \ + requires optional_like> \ + auto filter_deref(Seq seq) -> sequence auto; + + Given a sequence of "optional-like" elements (e.g. :type:`std::optional`, :type:`flux::optional`, :expr:`T*` etc...), filters out those elements which return :texpr:`false` after conversion to :texpr:`bool`, and performs a dereference of the remaining elements. + + Equivalent to :expr:`filter_map(seq, std::identity{})`. + + :models: + + .. list-table:: + :align: left + :header-rows: 1 + + * - Concept + - When + * - :concept:`multipass_sequence` + - :var:`Seq` is multipass + * - :concept:`bidirectional_sequence` + - :var:`Seq` is bidirectional + * - :concept:`random_access_sequence` + - Never + * - :concept:`contiguous_sequence` + - Never + * - :concept:`bounded_sequence` + - :var:`Seq` is bounded + * - :concept:`sized_sequence` + - Never + * - :concept:`infinite_sequence` + - :var:`Seq` is infinite + * - :concept:`read_only_sequence` + - :var:`Seq` is read-only + * - :concept:`const_iterable_sequence` + - :var:`Seq` is const-iterable and :var:`Func` is const-invocable + + :see also: + + * :func:`flux::filter_map` + +``filter_map`` +^^^^^^^^^^^^^^ + +.. function:: + template \ + requires std::invocable> && \ + optional_like>> \ + auto filter_map(Seq seq, Func func) -> sequence auto; + + Performs both filtering and mapping using a single function. + Given a unary function :var:`func` returning an "optional-like" type, the returned adaptor filters out those elements for which :var:`func` returns a "disengaged" optional (that is, those which return :texpr:`false` after conversion to :texpr:`bool`). It then dereferences the remaining elements using :expr:`operator*`. + Equivalent to:: + + map(seq, func) + .filter([](auto&& arg) { return static_cast(arg) }) + .map([](auto&& arg) -> decltype(auto) { return *std::forward(arg); }); + + :models: + + .. list-table:: + :align: left + :header-rows: 1 + + * - Concept + - When + * - :concept:`multipass_sequence` + - :var:`Seq` is multipass + * - :concept:`bidirectional_sequence` + - :var:`Seq` is bidirectional + * - :concept:`random_access_sequence` + - Never + * - :concept:`contiguous_sequence` + - Never + * - :concept:`bounded_sequence` + - :var:`Seq` is bounded + * - :concept:`sized_sequence` + - Never + * - :concept:`infinite_sequence` + - :var:`Seq` is infinite + * - :concept:`read_only_sequence` + - :var:`Seq` is read-only + * - :concept:`const_iterable_sequence` + - :var:`Seq` is const-iterable and :var:`Func` is const-invocable + ``flatten`` ^^^^^^^^^^^ diff --git a/include/flux.hpp b/include/flux.hpp index 3721fa97..40e87701 100644 --- a/include/flux.hpp +++ b/include/flux.hpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include diff --git a/include/flux/core/concepts.hpp b/include/flux/core/concepts.hpp index faa1659c..d3914c1f 100644 --- a/include/flux/core/concepts.hpp +++ b/include/flux/core/concepts.hpp @@ -379,6 +379,18 @@ template requires detail::has_nested_sequence_traits struct sequence_traits : T::flux_sequence_traits {}; +namespace detail { + +template +concept optional_like = + std::default_initializable && + std::movable && + requires (O& o) { + { static_cast(o) }; + { *o } -> flux::detail::can_reference; + }; + +} } // namespace flux diff --git a/include/flux/core/inline_sequence_base.hpp b/include/flux/core/inline_sequence_base.hpp index b4162ba3..d7b9b7ea 100644 --- a/include/flux/core/inline_sequence_base.hpp +++ b/include/flux/core/inline_sequence_base.hpp @@ -271,6 +271,15 @@ struct inline_sequence_base { [[nodiscard]] constexpr auto filter(Pred pred) &&; + template + requires std::invocable> && + detail::optional_like>> + [[nodiscard]] + constexpr auto filter_map(Func func) &&; + + [[nodiscard]] + constexpr auto filter_deref() && requires detail::optional_like>; + [[nodiscard]] constexpr auto flatten() && requires sequence>; diff --git a/include/flux/core/simple_sequence_base.hpp b/include/flux/core/simple_sequence_base.hpp index 2a61c82c..905c7bd9 100644 --- a/include/flux/core/simple_sequence_base.hpp +++ b/include/flux/core/simple_sequence_base.hpp @@ -16,15 +16,6 @@ struct simple_sequence_base : inline_sequence_base {}; namespace detail { -template -concept optional_like = - std::default_initializable && - std::movable && - requires (O& o) { - { static_cast(o) }; - { *o } -> flux::detail::can_reference; - }; - template concept simple_sequence = std::derived_from> && diff --git a/include/flux/op/filter_map.hpp b/include/flux/op/filter_map.hpp new file mode 100644 index 00000000..1adb4e10 --- /dev/null +++ b/include/flux/op/filter_map.hpp @@ -0,0 +1,72 @@ + +// Copyright (c) 2024 Tristan Brindle (tcbrindle at gmail dot com) +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +#ifndef FLUX_OP_FILTER_MAP_HPP_INCLUDED +#define FLUX_OP_FILTER_MAP_HPP_INCLUDED + +#include +#include + +namespace flux { + +namespace detail { + +struct filter_map_fn { + // If dereffing the optional would give us an rvalue reference, + // prevent a probable dangling reference by returning by value instead + template + using strip_rvalue_ref_t = std::conditional_t< + std::is_rvalue_reference_v, std::remove_reference_t, T>; + + template + requires (std::invocable> && + optional_like>>>) + constexpr auto operator()(Seq&& seq, Func func) const + { + return flux::map(FLUX_FWD(seq), std::move(func)) + .filter([](auto&& opt) { return static_cast(opt); }) + .map([](auto&& opt) -> strip_rvalue_ref_t { + return *FLUX_FWD(opt); + }); + } +}; + +} // namespace detail + +FLUX_EXPORT inline constexpr auto filter_map = detail::filter_map_fn{}; + +template +template +requires std::invocable> && + detail::optional_like>> +constexpr auto inline_sequence_base::filter_map(Func func) && +{ + return flux::filter_map(derived(), std::move(func)); +} + +namespace detail +{ + +struct filter_deref_fn { + template + requires optional_like> + constexpr auto operator()(Seq&& seq) const + { + return filter_map(FLUX_FWD(seq), [](auto&& opt) -> decltype(auto) { return FLUX_FWD(opt); }); + } +}; + +} // namespace detail + +FLUX_EXPORT inline constexpr auto filter_deref = detail::filter_deref_fn{}; + +template +constexpr auto inline_sequence_base::filter_deref() && requires detail::optional_like> +{ + return flux::filter_deref(derived()); +} +} // namespace flux + +#endif diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7f7e58b0..fe5d47d9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -43,6 +43,7 @@ add_executable(test-flux test_equal.cpp test_fill.cpp test_filter.cpp + test_filter_map.cpp test_find.cpp test_find_min_max.cpp test_flatten.cpp diff --git a/test/test_filter_map.cpp b/test/test_filter_map.cpp new file mode 100644 index 00000000..f47044ac --- /dev/null +++ b/test/test_filter_map.cpp @@ -0,0 +1,137 @@ + +// Copyright (c) 2024 Tristan Brindle (tcbrindle at gmail dot com) +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + +#include "catch.hpp" + +#include +#include +#include + +#include "test_utils.hpp" + +namespace { + +constexpr auto is_even_opt = [](int i) { return i % 2 == 0 ? std::optional{i} : std::nullopt; }; + +struct Pair { + int i; + bool ok; + + [[nodiscard]] constexpr auto map_if_ok() const { return ok ? std::optional{*this} : std::nullopt; } + + constexpr bool operator==(const Pair&) const = default; +}; + +using filter_fn = decltype(flux::filter_map); + +// int is not a sequence +static_assert(not std::invocable); +// int is not a function +static_assert(not std::invocable); +// "func" does not return optional_like +static_assert(not std::invocable); +// Incompatible predicate +static_assert(not std::invocable); + +constexpr bool test_filter() +{ + // Basic filtering + { + int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + auto filtered = flux::filter_map(flux::ref(arr), is_even_opt); + using F = decltype(filtered); + static_assert(flux::sequence); + static_assert(flux::bidirectional_sequence); + static_assert(flux::bounded_sequence); + static_assert(not flux::ordered_cursor); + static_assert(not flux::sized_sequence); + + static_assert(flux::sequence); + static_assert(flux::bidirectional_sequence); + static_assert(flux::bounded_sequence); + static_assert(not flux::ordered_cursor); + static_assert(not flux::sized_sequence); + + STATIC_CHECK(check_equal(filtered, {0, 2, 4, 6, 8})); + STATIC_CHECK(check_equal(std::as_const(filtered), {0, 2, 4, 6, 8})); + } + + // A predicate that always returns true returns what it was given + { + int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + auto filtered = flux::filter_map(flux::ref(arr), [](auto&& i) { return std::optional{i}; }); + + STATIC_CHECK(check_equal(arr, filtered)); + } + + // A predicate that always returns false returns an empty sequence + { + int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + auto filtered = flux::filter_map(flux::ref(arr), [](auto&&) -> std::optional { return std::nullopt; }); + + STATIC_CHECK(filtered.is_empty()); + } + + // We can use any optional_like such as a pointer + { + int i = 1, j = 3; + std::array arr{&i, nullptr, &j, nullptr}; + + auto filtered = flux::filter_map(arr, [](auto ptr) { return ptr; }); + + STATIC_CHECK(check_equal(filtered, {1, 3})); + } + + // ... Better expressed as filter_deref + { + int i = 1, j = 3; + std::array arr{&i, nullptr, &j, nullptr}; + + auto filtered = flux::filter_deref(arr); + + STATIC_CHECK(check_equal(filtered, {1, 3})); + } + + // We can use a PMF to filter_map + { + std::array pairs = { + Pair{1, true}, + {2, false}, + {3, true}, + {4, false} + }; + + auto f = flux::filter_map(pairs, &Pair::map_if_ok); + + STATIC_CHECK(check_equal(f, {Pair{1, true}, Pair{3, true}})); + } + + // Reversed sequences can be filtered + { + int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + auto filtered = flux::ref(arr).reverse().filter_map(is_even_opt); + + STATIC_CHECK(check_equal(filtered, {8, 6, 4, 2, 0})); + } + + // ... and filtered sequences can be reversed + { + int arr[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; + auto filtered = flux::filter_map(flux::ref(arr), is_even_opt).reverse(); + + STATIC_CHECK(check_equal(filtered, {8, 6, 4, 2, 0})); + } + + return true; +} +static_assert(test_filter()); + +} + +TEST_CASE("filter_map") +{ + bool result = test_filter(); + REQUIRE(result); +}