diff --git a/CHANGELOG.md b/CHANGELOG.md index af871fe1..39c466e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ Design history of libfn, newest first. The living documents — [README.md](README.md), [CONTRIBUTING.md](CONTRIBUTING.md), [docs/](docs/) — describe only the present state of the design; when a decision makes an earlier idea obsolete, this file is where the transition is recorded and explained. +## `optional` refuses pack and copack referents — 3 October 2026 + +`fn::optional` no longer accepts a `pack` or `copack` referent, matching `just`. `fn::optional&>` dispatched on its referent's active alternative, and `fn::optional&>` expanded its referent's fields. How a reference to a product or a sum takes part in conjunction, disjunction and grading is an open question, so both are refused until the type algebra answers it; issue #434 discusses the copack case. `pfn::optional` does not reject them, but `fn` and `pfn` types are not meant to be used together. + ## `` becomes ``; `monadic_invocable` rejects an incomplete functor — 27 September 2026 Replace includes of `` with ``; no compatibility header is provided. `some_in_place_type` moves to `` and remains available through ``. @@ -44,6 +48,21 @@ Results are converted before spliced arguments expire, fixing a dangling referen `void` lifts to `copack>`, including in `same_value_kind` and pipeline recovery into `optional`. A recovery returned by reference from `optional` has its copy included in the `noexcept` specification. +## `just` holds lvalue references — 24 September 2026 + +`just` now supports lvalue-reference payloads (issue #417), providing an always-engaged counterpart to `optional`. Referents must be object types other than arrays, in-place type tags, packs, or copacks. Rvalue-reference payloads remain unsupported, as in `optional` and `pack`. + +`just` follows `optional`: copy assignment and `emplace` rebind, comparisons compare the referents, and `value_type` is `T`. `value()` returns `T&` regardless of the carrier's value category or constness. Callable operations use that same reference, expanding tuple-like referents as `fn::apply` does. `apply_type` tags the payload with `std::in_place_type`. + +There is no default constructor. For an `int x`, `just{x}` still deduces `just`; the explicit type tag in `just(std::in_place_type, x)` instead deduces `just`. As with the library's `optional`, binding to a temporary is not yet rejected and can leave a dangling reference. + +Composition follows these rules: + +- A `transform` callback returning a supported lvalue reference `U&` produces `just`, referring to the returned object. An rvalue owning `just` rejects such a result: it may refer into the payload, which expires with the carrier. `optional` accepts it, as the standard specifies; `just` deliberately diverges. +- Member and pipeline `and_then` accept callbacks returning `just`. When every branch of a choice returns the same `just` type, the result retains that type. A join of different result types is rejected if any is a reference `just`, because references cannot be alternatives of the resulting choice. This matches the restriction on reference payloads in optional joins. +- The products and sums that `&` and `|` build hold a copy of the referent, as they do for `optional`. This includes a product with the unit `just`: `just{} & just{x}` is `just>`. +- Copack references remain unsupported. `just&>` would dispatch on the referent's active alternative, introducing control flow based on state the carrier does not own. Issue #434 discusses the tradeoffs and open questions. `transform` also rejects copack-reference results. + ## Singular copacks support the tuple protocol — 24 September 2026 A `copack` with exactly one alternative supports the tuple protocol: `std::tuple_size_v>` is 1, `std::tuple_element_t<0, copack>` is `T`, and `get<0>` returns the same reference as the index-less `get`. This supports structured bindings and generic code that uses tuple traits with ADL `get`. A structured binding over a singular copack, including a singular choice's `value()`, now binds its alternative. Previously, it bound the public `data` and `index` members. diff --git a/TYPE_ALGEBRA.md b/TYPE_ALGEBRA.md index 854a0fd3..d68cfc5f 100644 --- a/TYPE_ALGEBRA.md +++ b/TYPE_ALGEBRA.md @@ -1040,14 +1040,14 @@ The library respects C++ value mechanics: - `noexcept` is conditionally computed. - Value categories (lvalue/rvalue) propagate strictly to callbacks, avoiding copies. - Immovable and move-only payloads are supported in place. -- Reference-bearing `pack` and `optional` are supported. Lifetime management of non-owning references remains with the caller. +- Reference-bearing `pack`, `optional` and `just` are supported. Lifetime management of non-owning references remains with the caller. - `pack` compares element-wise, supporting equality and three-way comparison. For reference-bearing `pack`, comparison applies to the referents rather than the references themselves. > [!NOTE] > > ### Note — reference payloads > -> Raw reference payloads are disallowed on the carriers `expected`, `just` and `choice`, and as `copack` alternatives. `expected` stores its payload in a union, and C++ forbids a union member of reference type; the algebra's own types refuse them so that every alternative is dispatched the same way, whatever it holds. `optional` is the deliberate exception — the standard specifies it, and `libfn` polyfills it. If you want to propagate references inside the other carriers, wrap them in a `pack` (e.g. `expected, E>`). +> Raw reference payloads are disallowed on the carriers `expected` and `choice`, and as `copack` alternatives. `expected` stores its payload in a union, and C++ forbids a union member of reference type. `copack` excludes reference alternatives so that alternatives follow the same dispatch rules. `optional` and its always-engaged counterpart, `just`, support lvalue-reference payloads. Assignment rebinds the reference. Callable operations use `T&` regardless of the carrier's value category or constness, expanding tuple-like referents as `fn::apply` does. `libfn` polyfills the standard's `optional`. Both exclude references to packs and copacks: how a reference to a product or a sum would take part in conjunction, disjunction and grading is an open question. An owning `just` whose `transform` callable returns an lvalue reference produces a reference `just` only if the carrier is an lvalue; an rvalue carrier rejects it, as the reference may refer into the payload that expires with the carrier. `optional` accepts it, as the standard specifies. If you want to propagate references inside the other carriers, wrap them in a `pack` (e.g. `expected, E>`). ```cpp diff --git a/docs/reference/just.md b/docs/reference/just.md index 38eab945..370d1314 100644 --- a/docs/reference/just.md +++ b/docs/reference/just.md @@ -8,6 +8,8 @@ title: "monad fn::just" :include-doxygen-doc: fn::just +:include-doxygen-doc: fn::just< T & > + ## Member types {style: "api"} ```cpp {title: "fn::just::value_type"} @@ -28,6 +30,18 @@ using value_type = void; // (1) :include-doxygen-doc: fn::just< void >::value_type { args: "" } +```cpp {title: "fn::just< T & >::value_type"} +using value_type = T; // (1) +``` + +:include-doxygen-doc: fn::just< T & >::value_type { args: "" } + +```cpp {title: "fn::just< T & >::p_"} +T * p_; // (1) +``` + +:include-doxygen-doc: fn::just< T & >::p_ { args: "" } + ## Construction {style: "api"} ```cpp {title: "fn::just< void >::just"} @@ -69,6 +83,24 @@ constexpr explicit just(std::in_place_type_t, Args &&...args); // (6) :include-doxygen-doc-params: fn::just::just { args: "::std::in_place_type_t< T >, Args &&...", title: "parameters" } +```cpp {title: "fn::just< T & >::just"} +constexpr just(just const &) = default; // (1) + +template +constexpr explicit just(U &&u); // (2) +constexpr explicit just(std::in_place_type_t, U &&u); // (3) +``` + +:include-doxygen-doc: fn::just< T & >::just { args: "just const &" } + +:include-doxygen-doc: fn::just< T & >::just { args: "U &&" } + +:include-doxygen-doc-params: fn::just< T & >::just { args: "U &&", title: "parameters" } + +:include-doxygen-doc: fn::just< T & >::just { args: "::std::in_place_type_t< T & >, U &&" } + +:include-doxygen-doc-params: fn::just< T & >::just { args: "::std::in_place_type_t< T & >, U &&", title: "parameters" } + ## Destructor {style: "api"} ```cpp {title: "fn::just::~just"} @@ -77,6 +109,12 @@ constexpr ~just() = default; // (1) :include-doxygen-doc: fn::just::~just { args: "" } +```cpp {title: "fn::just< T & >::~just"} +constexpr ~just() = default; // (1) +``` + +:include-doxygen-doc: fn::just< T & >::~just { args: "" } + ## emplace {style: "api"} ```cpp {title: "fn::just::emplace"} @@ -87,6 +125,15 @@ constexpr auto emplace(auto &&...args) -> T &; // (1) :include-doxygen-doc-params: fn::just::emplace { args: "auto &&...", title: "parameters" } +```cpp {title: "fn::just< T & >::emplace"} +template +constexpr auto emplace(U &&u) -> T &; // (1) +``` + +:include-doxygen-doc: fn::just< T & >::emplace { args: "U &&" } + +:include-doxygen-doc-params: fn::just< T & >::emplace { args: "U &&", title: "parameters" } + ## Assignment {style: "api"} ```cpp {title: "fn::just::operator="} @@ -105,6 +152,12 @@ constexpr auto operator=(U &&v) -> just &; // (3) :include-doxygen-doc-params: fn::just::operator= { args: "U &&", title: "parameters" } +```cpp {title: "fn::just< T & >::operator="} +constexpr auto operator=(just const &) = default -> just &; // (1) +``` + +:include-doxygen-doc: fn::just< T & >::operator= { args: "just const &" } + ## operator== {style: "api"} ```cpp {title: "fn::just< void >::operator=="} @@ -134,6 +187,14 @@ constexpr auto value() const -> void; // (1) :include-doxygen-doc: fn::just< void >::value { args: "" } +```cpp {title: "fn::just< T & >::value"} +constexpr auto value() const -> T &; // (1) +``` + +:include-doxygen-doc: fn::just< T & >::value { args: "" } + +:include-doxygen-doc-params: fn::just< T & >::value { args: "", title: "parameters" } + ## transform {style: "api"} ```cpp {title: "fn::just::transform"} @@ -157,6 +218,15 @@ constexpr auto transform(Fn &&fn) const; // (1) :include-doxygen-doc-params: fn::just< void >::transform { args: "Fn &&", title: "parameters" } +```cpp {title: "fn::just< T & >::transform"} +template +constexpr auto transform(Fn &&fn) const; // (1) +``` + +:include-doxygen-doc: fn::just< T & >::transform { args: "Fn &&" } + +:include-doxygen-doc-params: fn::just< T & >::transform { args: "Fn &&", title: "parameters" } + ## and_then {style: "api"} ```cpp {title: "fn::just::and_then"} @@ -180,6 +250,15 @@ constexpr auto and_then(Fn &&fn) const; // (1) :include-doxygen-doc-params: fn::just< void >::and_then { args: "Fn &&", title: "parameters" } +```cpp {title: "fn::just< T & >::and_then"} +template +constexpr auto and_then(Fn &&fn) const; // (1) +``` + +:include-doxygen-doc: fn::just< T & >::and_then { args: "Fn &&" } + +:include-doxygen-doc-params: fn::just< T & >::and_then { args: "Fn &&", title: "parameters" } + ## apply {style: "api"} ```cpp {title: "fn::just::apply"} @@ -203,6 +282,15 @@ constexpr auto apply(Fn &&fn, Args &&...args) const -> decltype(auto); // (1) :include-doxygen-doc-params: fn::just< void >::apply { args: "Fn &&, Args &&...", title: "parameters" } +```cpp {title: "fn::just< T & >::apply"} +template +constexpr auto apply(Fn &&fn, Args &&...args) const -> decltype(auto); // (1) +``` + +:include-doxygen-doc: fn::just< T & >::apply { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::just< T & >::apply { args: "Fn &&, Args &&...", title: "parameters" } + ## apply_r {style: "api"} ```cpp {title: "fn::just::apply_r"} @@ -230,6 +318,17 @@ constexpr auto apply_r(Fn &&fn, Args &&...args) const -> Ret; // (1) :include-doxygen-doc-params: fn::just< void >::apply_r { args: "Fn &&, Args &&...", title: "parameters" } +```cpp {title: "fn::just< T & >::apply_r"} +template +constexpr auto apply_r(Fn &&fn, Args &&...args) const -> Ret; // (1) +``` + +:include-doxygen-doc: fn::just< T & >::apply_r { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::just< T & >::apply_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::just< T & >::apply_r { args: "Fn &&, Args &&...", title: "parameters" } + ## apply_type {style: "api"} ```cpp {title: "fn::just::apply_type"} @@ -253,6 +352,15 @@ constexpr auto apply_type(Fn &&fn, Args &&...args) const -> decltype(auto); // :include-doxygen-doc-params: fn::just< void >::apply_type { args: "Fn &&, Args &&...", title: "parameters" } +```cpp {title: "fn::just< T & >::apply_type"} +template +constexpr auto apply_type(Fn &&fn, Args &&...args) const -> decltype(auto); // (1) +``` + +:include-doxygen-doc: fn::just< T & >::apply_type { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::just< T & >::apply_type { args: "Fn &&, Args &&...", title: "parameters" } + ## apply_type_r {style: "api"} ```cpp {title: "fn::just::apply_type_r"} @@ -279,3 +387,14 @@ constexpr auto apply_type_r(Fn &&fn, Args &&...args) const -> Ret; // (1) :include-doxygen-doc-params: fn::just< void >::apply_type_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" } :include-doxygen-doc-params: fn::just< void >::apply_type_r { args: "Fn &&, Args &&...", title: "parameters" } + +```cpp {title: "fn::just< T & >::apply_type_r"} +template +constexpr auto apply_type_r(Fn &&fn, Args &&...args) const -> Ret; // (1) +``` + +:include-doxygen-doc: fn::just< T & >::apply_type_r { args: "Fn &&, Args &&..." } + +:include-doxygen-doc-params: fn::just< T & >::apply_type_r { args: "Fn &&, Args &&...", type: "template", title: "template parameters" } + +:include-doxygen-doc-params: fn::just< T & >::apply_type_r { args: "Fn &&, Args &&...", title: "parameters" } diff --git a/include/fn/copack.hpp b/include/fn/copack.hpp index 2721112b..6bef05ef 100644 --- a/include/fn/copack.hpp +++ b/include/fn/copack.hpp @@ -209,6 +209,9 @@ template struct superset; template struct superset> { using type = ::fn::just::template apply<::fn::copack>>; }; + +template constexpr inline bool is_reference_just = false; +template constexpr inline bool is_reference_just<::fn::just> = true; } // namespace _joining_superset // Rs are the branch results, cv/ref stripped @@ -220,6 +223,12 @@ template struct _joining_superset_type { using type = R0; }; +// References cannot be choice alternatives. If any branch returns a reference just, +// all branches must return the same carrier type, as in optional joins. +template + requires(not(... && ::std::is_same_v)) + && (_joining_superset::is_reference_just || ... || _joining_superset::is_reference_just) +struct _joining_superset_type {}; // An all-just result set uses the join above. Other results must agree in exact type; // the member then rejects non-just results. Divergent results trigger select's assertion. diff --git a/include/fn/just.hpp b/include/fn/just.hpp index e979ccd5..6c41a2dc 100644 --- a/include/fn/just.hpp +++ b/include/fn/just.hpp @@ -44,19 +44,30 @@ template concept some_choice = detail::_some_choice; namespace detail { +// Supported referents for just. Packs and copacks are excluded: how a reference to a product +// or a sum takes part in conjunction, disjunction and grading is an open question (issue #434). +template +concept _just_referent + = ::std::is_object_v && (not ::std::is_array_v) && (not _some_in_place_type<::std::remove_cv_t>) + && (not _some_pack) && (not _some_copack); + // The payload just admits - the class mandates below assert the same set, and the transform verb // asks this before naming just anywhere, so an inadmissible result answers instead of // firing a mandate inside the probe. The empty copack is out: just> is incomplete. template concept _just_payload - = (not ::std::is_same_v>) && (not ::std::is_reference_v) && (not ::std::is_array_v) - && (not _some_in_place_type) && ::std::is_same_v>; + = (::std::is_lvalue_reference_v && _just_referent<::std::remove_reference_t>) + || ((not ::std::is_same_v>) && (not ::std::is_reference_v) && (not ::std::is_array_v) + && (not _some_in_place_type) && ::std::is_same_v>); // What the callback's result may become: void and admissible payloads make a just; anything else -// keeps the member viable-but-loud below +// keeps the member viable-but-loud below. A reference result from an rvalue payload is out: it may +// refer into the payload, which expires with the carrier. template -concept _just_admissible_result - = ::std::is_void_v::type> || _just_payload::type>; +concept _just_admissible_result = ::std::is_void_v::type> + || (_just_payload::type> + && not(::std::is_reference_v::type> + && (... || ::std::is_rvalue_reference_v))); // For an admissible result, transform returns just. Otherwise expose the raw result // type (including empty copacks, references and arrays) so probing does not instantiate an @@ -84,6 +95,7 @@ constexpr inline struct _just_from_invoke_t { // Declare the specializations before the primary can instantiate them through transform. // just> remains incomplete because there is no alternative to hold; just is the unit. template struct just; +template struct just; template <> struct just; template struct just>; template <> struct just>; @@ -254,6 +266,10 @@ template struct just { /** * @brief Maps the payload through the callable, wrapping the result * + * A callable returning an lvalue reference makes a `just` of that reference, but only from an + * lvalue carrier: an rvalue carrier rejects it, as the reference may refer into its expiring + * payload. + * * @param fn Callable applied on the payload * @return `just` of the callable's result; `just` for a void result */ @@ -695,6 +711,226 @@ template <> struct just { } }; +/** + * @brief The identity carrier over an lvalue reference: always bound to one `T` + * + * The always-engaged counterpart of `optional`. Copy assignment and `emplace` rebind the + * reference without assigning to the referent. `value()` returns `T&` regardless of the carrier's + * value category or constness. Callable operations use the same reference, expanding tuple-like + * referents as `fn::apply` does. + * + * There is no default constructor. Deduction from a value produces an owning carrier; use + * `just` or `std::in_place_type` to request a reference. Referents must be object types + * other than arrays, in-place type tags, packs, or copacks. The carrier is trivially copyable and a + * structural type. + * + * The caller must keep the referent alive. As with the library's `optional`, binding to a + * temporary is not rejected and can leave a dangling reference. Detecting such bindings requires + * the C++23 trait `std::reference_constructs_from_temporary`. + * + * @tparam T Referent type + */ +template struct just { + static_assert(detail::_just_referent); + + /** + * @brief The referent type, including any cv-qualification + */ + using value_type = T; + + /** + * @brief The referent's address + */ + T *p_; + + /** + * @brief Copy constructor; binds the same referent + */ + constexpr just(just const &) = default; + /** + * @brief Copy assignment; rebinds to the other's referent + */ + constexpr just &operator=(just const &) = default; + /** + * @brief Destructor + */ + constexpr ~just() = default; + + /** + * @brief Binds the reference to the argument + * + * Explicit unless the argument is implicitly convertible to `T&`. + * + * @param u The referent, or a value whose conversion yields it + */ + template + requires(not ::std::is_same_v<::std::remove_cvref_t, just>) + && (not detail::_some_in_place_type<::std::remove_cvref_t>) && ::std::is_constructible_v + constexpr explicit(not ::std::is_convertible_v) just(U &&u) // NOSONAR cpp:S6458 the constraint excludes self + noexcept(::std::is_nothrow_constructible_v) + : p_(_address(FWD(u))) + { + } + + /** + * @brief Binds the reference to the argument + * + * @param u The referent, or a value whose conversion yields it + */ + template + requires ::std::is_constructible_v + constexpr explicit just(::std::in_place_type_t, U &&u) // + noexcept(::std::is_nothrow_constructible_v) + : p_(_address(FWD(u))) + { + } + + /** + * @brief Rebinds the reference to the argument + * + * @param u The new referent, or a value whose conversion yields it + * @return Reference to the new referent + */ + template + constexpr T &emplace(U &&u) noexcept(::std::is_nothrow_constructible_v) + requires ::std::is_constructible_v + { + p_ = _address(FWD(u)); + return *p_; + } + + /** + * @brief Accesses the referent + * + * @return The referent, whatever the value category of `*this` + */ + [[nodiscard]] constexpr T &value() const noexcept { return *p_; } + + /** + * @brief Maps the referent through the callable, wrapping the result + * + * A supported lvalue-reference result produces a non-owning `just`; a value result + * produces an owning carrier. + * + * @param fn Callable applied on the referent + * @return `just` of the callable's result; `just` for a void result + */ + template + [[nodiscard]] constexpr auto transform(Fn &&fn) const // + noexcept(detail::_is_nothrow_applicable::value) -> typename detail::_just_transform_result::type + requires detail::_is_applicable::value + { + using type = detail::_apply_result::type; + static_assert(detail::_just_admissible_result); + if constexpr (::std::is_void_v) { + detail::_apply(FWD(fn), *p_); + return just{}; + } else if constexpr (detail::_just_payload) + return just{detail::_just_from_invoke, + [&fn, this]() -> decltype(auto) { return detail::_apply(FWD(fn), *p_); }}; + else + ::pfn::unreachable(); // LCOV_EXCL_LINE - rejected by the static_assert above + } + + /** + * @brief Binds the referent through the callable, which returns a `just` of any payload + * + * @param fn Callable applied on the referent + * @return The callable's `just` result, returned by value + */ + template + [[nodiscard]] constexpr auto and_then(Fn &&fn) const // + noexcept(detail::_is_nothrow_applicable::value) + -> ::std::remove_cvref_t::type> + requires detail::_is_applicable::value + { + // the member is the carrier's own bind; the cross-carrier bind lives in the and_then functor + static_assert(some_just::type>); + return detail::_apply(FWD(fn), *p_); + } + + /** + * @brief Eliminates the referent through the callable + * + * As with `fn::apply`, tuple-like referents are expanded into their elements; other + * referents are passed whole. Additional arguments follow the referent or its elements. + * + * @param fn Callable applied on the referent + * @param args Additional arguments, appended after the referent's content + * @return The callable's result + */ + template + [[nodiscard]] constexpr auto apply(Fn &&fn, Args &&...args) const // + noexcept(detail::_is_nothrow_applicable::value) -> decltype(auto) + requires detail::_is_applicable::value + { + return detail::_apply(FWD(fn), *p_, FWD(args)...); + } + + /** + * @brief Eliminates the referent through the callable, converting the result to `Ret` + * + * @tparam Ret Type the result converts to + * @param fn Callable applied on the referent + * @param args Additional arguments, appended after the referent's content + * @return The callable's result, converted to `Ret` + */ + template + [[nodiscard]] constexpr auto apply_r(Fn &&fn, Args &&...args) const // + noexcept(detail::_is_nothrow_applicable_r::value) -> Ret + requires detail::_is_applicable_r::value + { + return detail::_apply_r(FWD(fn), *p_, FWD(args)...); + } + + /** + * @brief Eliminates the referent through the callable, keyed by the payload's type + * + * The callable receives `std::in_place_type`, then the referent or its elements as in + * `apply`, then any additional arguments. + * + * @param fn Callable applied on the tag and the referent + * @param args Additional arguments, appended after the referent's content + * @return The callable's result + */ + template + [[nodiscard]] constexpr auto apply_type(Fn &&fn, Args &&...args) const // + noexcept(noexcept(detail::_apply_tagged<::std::in_place_type_t>(FWD(fn), *p_, FWD(args)...))) + -> decltype(auto) + requires requires { detail::_apply_tagged<::std::in_place_type_t>(FWD(fn), *p_, FWD(args)...); } + { + return detail::_apply_tagged<::std::in_place_type_t>(FWD(fn), *p_, FWD(args)...); + } + + /** + * @brief Eliminates the referent through the callable, keyed by the payload's type, converting + * the result to `Ret` + * + * @tparam Ret Type the result converts to + * @param fn Callable applied on the tag and the referent + * @param args Additional arguments, appended after the referent's content + * @return The callable's result, converted to `Ret` + */ + template + [[nodiscard]] constexpr auto apply_type_r(Fn &&fn, Args &&...args) const // + noexcept(noexcept(detail::_apply_tagged_r>(FWD(fn), *p_, FWD(args)...))) -> Ret + requires requires { detail::_apply_tagged_r>(FWD(fn), *p_, FWD(args)...); } + { + return detail::_apply_tagged_r>(FWD(fn), *p_, FWD(args)...); + } + +private: + template friend struct just; + + // Same semantics as `T &r(FWD(u));`, spelled as a cast for MSVC (error C2440) + template static constexpr T *_address(U &&u) noexcept(::std::is_nothrow_constructible_v) + { + return ::std::addressof(static_cast(FWD(u))); + } + + template constexpr explicit just(detail::_just_from_invoke_t, Fn &&make) : p_(_address(FWD(make)())) {} +}; + namespace detail { // Inject each branch's payload into the joined choice: a choice contributes its copack, // an ordinary just contributes its value, and just contributes pack<>. Constructing from diff --git a/include/fn/optional.hpp b/include/fn/optional.hpp index 29c42b8c..03e7ee91 100644 --- a/include/fn/optional.hpp +++ b/include/fn/optional.hpp @@ -37,6 +37,11 @@ concept some_optional = detail::_some_optional; namespace detail { +// Packs and copacks are excluded as referents of optional: how a reference to a product or a +// sum takes part in conjunction, disjunction and grading is an open question (issue #434). +template +concept _optional_referent = (not _some_pack) && (not _some_copack); + // [optional.iterators]: the implementation-defined iterator types for fn::optional. A // minimal wrapper over T* whose job is to keep pointer-ness out of optional interface. template class _optional_iterator { @@ -1072,7 +1077,8 @@ template optional(T) -> optional; * * As `std::optional` is specified for C++26 - the one carrier that holds a raw reference. * Lifetime responsibility for the referent stays with the caller. The extensions mirror - * `optional`'s, the callable always receiving a plain `T&`. + * `optional`'s, the callable always receiving a plain `T&`. The referent cannot be a `pack` or + * a `copack`. * * @tparam T Referent type */ @@ -1082,6 +1088,7 @@ template optional(T) -> optional; // there is only ever one overload of each (no ref-qualifier/const overload set). template class optional : private detail::_optional_base { static_assert(::pfn::detail::_is_valid_optional); + static_assert(detail::_optional_referent); using _base = detail::_optional_base; // Allow sibling _optional_base instantiations to downcast into the private base. diff --git a/tests/fn/and_then.cpp b/tests/fn/and_then.cpp index f55b8d3e..25b3c2bd 100644 --- a/tests/fn/and_then.cpp +++ b/tests/fn/and_then.cpp @@ -53,6 +53,9 @@ struct Xint final { auto ofn4() const && noexcept -> fn::optional { return {value + 4}; } }; +template +concept member_and_then = requires(S s, Fn fn) { FWD(s).and_then(fn); }; + template struct Xfn final { auto operator()(Xint &v) const noexcept -> R { return {v.value + 1}; } auto operator()(Xint const &v) const noexcept -> R { return {v.value + 2}; } @@ -1080,6 +1083,54 @@ TEST_CASE("and_then across the identity cluster", "[and_then][just][choice][expe CHECK((fn::choice_for{B{}} | fn::and_then(fnUnit)) == fn::as_choice(2)); } + SECTION("reference payloads join only by converging") + { + int x = 1; + int y = 2; + // Matching reference-just results preserve the referent through member and pipeline calls. + auto const fnRef = fn::overload{[&x](A) { return fn::just{x}; }, // + [&y](B) { return fn::just{y}; }}; + auto r1 = fn::choice_for{B{}} | fn::and_then(fnRef); + static_assert(std::is_same_v>); + CHECK(&r1.value() == &y); + CHECK(&fn::choice_for{A{}}.and_then(fnRef).value() == &x); + auto r2 = fn::expected, E0>{fn::copack_for{A{}}} | fn::and_then(fnRef); + static_assert(std::is_same_v>); + CHECK(&r2.value() == &x); + static_assert([] { + int v = 1; + auto const fnView = fn::overload{[&v](A) { return fn::just{v}; }, // + [&v](B) { return fn::just{v}; }}; + // named source: the same VS 2022 misread as above + constexpr fn::choice_for cb{B{}}; + return &cb.and_then(fnView).value() == &v; + }()); + + // Mixing reference and value payloads is rejected: references cannot be choice alternatives. + auto const fnMixed = fn::overload{[&x](A) { return fn::just{x}; }, // + [](B) { return fn::just{2}; }}; + static_assert(not member_and_then &, decltype(fnMixed) const &>); + static_assert(not fn::applicable_and_then &>); + static_assert( + not fn::applicable_and_then_across, E0> &>); + // Owning payloads of different types still join. + constexpr auto fnOwned = fn::overload{[](A) { return fn::just{1}; }, // + [](B) { return fn::just{2}; }}; + static_assert(member_and_then &, decltype(fnOwned) const &>); + static_assert(fn::applicable_and_then_across, E0> &>); + + // A pipeline callback can return an optional referring to the same object. + fn::just j{x}; + auto r3 = j | fn::and_then([](int &i) { return fn::optional{i}; }); + static_assert(std::is_same_v>); + CHECK(&r3.value() == &x); + static_assert([] { + int v = 1; + auto o = fn::just{v} | fn::and_then([](int &i) { return fn::optional{i}; }); + return &o.value() == &v; + }()); + } + SECTION("just binds to a choice, and crosses to the identity expected") { auto r1 = fn::just{3} | fn::and_then([](int) { return fn::choice{U{}}; }); diff --git a/tests/fn/just.cpp b/tests/fn/just.cpp index fe82e362..12d894fe 100644 --- a/tests/fn/just.cpp +++ b/tests/fn/just.cpp @@ -38,6 +38,26 @@ struct ThrowingCtor final { bool operator==(ThrowingCtor const &) const = default; }; +// Referent sources whose binding goes through a conversion function: explicit and nothrow, or +// implicit and potentially throwing +struct ExplicitRef final { + int x; + constexpr explicit operator int &() noexcept { return x; } +}; +struct ImplicitRef final { + int x; + bool fail = false; + constexpr operator int &() // NOLINT(google-explicit-constructor) + { + if (fail) + throw 0; + return x; + } +}; + +constexpr int five = 5; +template J> constexpr int nttp_value = J.value(); + template concept can_and_then = requires(S s, Fn fn) { FWD(s).and_then(fn); }; template @@ -46,6 +66,10 @@ template concept can_apply = requires(S s, Fn fn, Args... args) { FWD(s).apply(fn, FWD(args)...); }; template concept can_apply_type = requires(S s, Fn fn, Args... args) { FWD(s).apply_type(fn, FWD(args)...); }; +template +concept can_apply_r = requires(S s, Fn fn) { FWD(s).template apply_r(fn); }; +template +concept can_apply_type_r = requires(S s, Fn fn) { FWD(s).template apply_type_r(fn); }; template concept implicitly_default_constructible = requires(void (&sink)(T)) { sink({}); }; template @@ -211,6 +235,17 @@ TEST_CASE("just", "[just]") // a void result wraps as the void carrier; an immovable result is constructed in place static_assert(std::is_same_v>); CHECK(a.transform([](int i) { return Immovable{i}; }).value().x == 3); + // an lvalue-reference result is a view of what the callback returned, here the payload + auto r4 = a.transform([](int &i) -> int & { return i; }); + static_assert(std::is_same_v>); + CHECK(&r4.value() == &a.value()); + // ... but not from an rvalue carrier, whose payload expires with it: the member stays + // viable-but-loud and yields no just to compose + constexpr auto fnView = [](int const &i) -> int const & { return i; }; + static_assert(can_transform); + static_assert(not fn::some_just); + static_assert(not fn::some_just); + static_assert(fn::some_just); // a copack result lands on the choice over its alternatives - the same carrier family constexpr auto fnCopack @@ -232,6 +267,11 @@ TEST_CASE("just", "[just]") static_assert(T{3}.transform([](int i) { return i + 1; }) == fn::just{4}); static_assert(T{3}.transform([](int i) { return Immovable{i}; }).value().x == 3); static_assert(T{3}.transform(fnCopack) == fn::choice{true}); + static_assert([] { + T t{3}; + auto v = t.transform([](int &i) -> int & { return i; }); + return &v.value() == &t.value(); + }()); SUCCEED(); } } @@ -367,6 +407,393 @@ TEST_CASE("just", "[just]") } } +TEST_CASE("just of reference", "[just]") +{ + using R = fn::just; + using C = fn::just; + + SECTION("constructors and deduction") + { + int i = 13; + R a{i}; + CHECK(&a.value() == &i); + R b(std::in_place_type, i); + CHECK(&b.value() == &i); + C c{i}; + CHECK(&c.value() == &i); + ExplicitRef e{7}; + CHECK(&R{e}.value() == &e.x); + + // Value deduction owns the payload; an explicit type tag can request a reference. + static_assert(std::is_same_v>); + static_assert(std::is_same_v, i)), R>); + + // a reference is born bound + static_assert(not std::is_default_constructible_v); + // Implicit construction follows implicit reference conversion. + static_assert(std::is_convertible_v); + static_assert(std::is_convertible_v); + static_assert(std::is_convertible_v); + static_assert(std::is_constructible_v); + static_assert(not std::is_convertible_v); + static_assert(std::is_constructible_v, int &>); + static_assert(not std::is_convertible_v, R>); + // noexcept follows the binding + static_assert(std::is_nothrow_constructible_v); + static_assert(std::is_nothrow_constructible_v); + static_assert(not std::is_nothrow_constructible_v); + // what int& cannot bind is refused, and another just is never unwrapped + static_assert(not std::is_constructible_v); + static_assert(not std::is_constructible_v); + static_assert(not std::is_constructible_v &>); + static_assert(not std::is_constructible_v, int &>); + + SECTION("throwing conversion") + { + ImplicitRef good{3}; + CHECK(&R{good}.value() == &good.x); + ImplicitRef bad{3, true}; + CHECK_THROWS_AS(R{bad}, int); + CHECK_THROWS_AS(R(std::in_place_type, bad), int); + } + + SECTION("constexpr") + { + static_assert([] { + int x = 1; + R r{x}; + R s(std::in_place_type, x); + ExplicitRef e{7}; + R t{e}; + ImplicitRef g{3}; + R u{g}; + return &r.value() == &x && &s.value() == &x && &t.value() == &e.x && &u.value() == &g.x; + }()); + SUCCEED(); + } + } + + SECTION("special members") + { + static_assert(std::is_trivially_copyable_v); + static_assert(std::is_trivially_copy_constructible_v); + static_assert(std::is_trivially_move_constructible_v); + static_assert(std::is_trivially_copy_assignable_v); + static_assert(std::is_trivially_move_assignable_v); + static_assert(std::is_trivially_destructible_v); + static_assert(std::is_same_v && std::is_same_v); + // a structural type: a constant just is a template argument + static_assert(nttp_value == 5); + SUCCEED(); + } + + SECTION("referent") + { + // Scalar references may be cv-qualified; rvalue, array, function, and tag references are rejected. + static_assert(fn::detail::_just_payload); + static_assert(fn::detail::_just_payload); + static_assert(not fn::detail::_just_payload); + static_assert(not fn::detail::_just_payload); + static_assert(not fn::detail::_just_payload); + static_assert(not fn::detail::_just_payload &>); + // Pack and copack referents are refused; owned packs and copacks are not. + static_assert(fn::detail::_just_payload>); + static_assert(not fn::detail::_just_payload &>); + static_assert(not fn::detail::_just_payload const &>); + static_assert(not fn::detail::_just_payload &>); + static_assert(fn::detail::_just_payload>); + static_assert(not fn::detail::_just_payload &>); + static_assert(not fn::detail::_just_payload const &>); + static_assert(not fn::detail::_just_payload &>); + // A borrowed choice is passed to the callable as a whole. + static_assert(fn::detail::_just_payload &>); + fn::choice ch{1}; + fn::just &> j{ch}; + CHECK(j.transform([](fn::choice &c) { return c == fn::choice{1}; }).value()); + static_assert([] { + fn::choice c{1}; + return fn::just &>{c}.transform([](fn::choice &v) { return &v; }).value() == &c; + }()); + } + + SECTION("assignment") + { + int x = 1; + int y = 2; + R a{x}; + // assignment rebinds, never assigning through: the previous referent is untouched + a = R{y}; + CHECK(&a.value() == &y); + CHECK(x == 1); + a = x; + CHECK(&a.value() == &x); + CHECK(y == 2); + // a throwing conversion leaves the binding as it was + ImplicitRef bad{3, true}; + CHECK_THROWS_AS(a = bad, int); + CHECK(&a.value() == &x); + + static_assert(std::is_nothrow_assignable_v); + static_assert(not std::is_nothrow_assignable_v); + static_assert(not std::is_assignable_v); + static_assert(not std::is_assignable_v &>); + + SECTION("constexpr") + { + static_assert([] { + int x = 1; + int y = 2; + R r{x}; + r = R{y}; + bool const rebound = &r.value() == &y && x == 1; + r = x; + return rebound && &r.value() == &x && y == 2; + }()); + SUCCEED(); + } + } + + SECTION("emplace") + { + int x = 1; + int y = 2; + R a{x}; + // emplace rebinds too, and returns the new referent + int &r = a.emplace(y); + CHECK(&r == &y); + CHECK(&a.value() == &y); + CHECK(x == 1); + ImplicitRef t{3}; + CHECK(&a.emplace(t) == &t.x); + // a throwing conversion leaves the binding as it was + ImplicitRef bad{4, true}; + CHECK_THROWS_AS(a.emplace(bad), int); + CHECK(&a.value() == &t.x); + + static_assert(std::is_same_v); + static_assert(noexcept(a.emplace(y))); + static_assert(not noexcept(a.emplace(t))); + + SECTION("constexpr") + { + static_assert([] { + int x = 1; + int y = 2; + R r{x}; + ImplicitRef g{3}; + return &r.emplace(y) == &y && &r.value() == &y && x == 1 && &r.emplace(g) == &g.x; + }()); + SUCCEED(); + } + } + + SECTION("value") + { + int x = 1; + R a{x}; + // Access returns an lvalue reference even through a const or rvalue carrier. + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v); + static_assert(std::is_same_v().value()), int const &>); + static_assert(noexcept(a.value())); + std::as_const(a).value() = 5; + CHECK(x == 5); + static_assert([] { + int y = 1; + R const r{y}; + r.value() = 5; + return y == 5; + }()); + } + + SECTION("transform") + { + int x = 3; + R a{x}; + constexpr auto fnLvalue = [](int &i) { return i + 1; }; + + SECTION("value category") + { + // the callable receives int& for every category of the carrier, never an rvalue + CHECK(a.transform(fnLvalue).value() == 4); + CHECK(std::as_const(a).transform(fnLvalue).value() == 4); + CHECK(std::move(a).transform(fnLvalue).value() == 4); + CHECK(std::move(std::as_const(a)).transform(fnLvalue).value() == 4); + constexpr auto fnRvalue = [](int &&i) { return i; }; + static_assert(not can_transform); + static_assert(can_transform &&, decltype(fnRvalue)>); + } + + SECTION("result") + { + // a reference result keeps a view, a value result owns, a void result is the unit + auto r1 = a.transform([](int &i) -> int & { return i; }); + static_assert(std::is_same_v); + CHECK(&r1.value() == &x); + auto r2 = a.transform([](int const &i) -> int const & { return i; }); + static_assert(std::is_same_v); + CHECK(&r2.value() == &x); + auto r3 = a.transform([](int i) { return long{i}; }); + static_assert(std::is_same_v>); + CHECK(r3.value() == 3L); + static_assert(std::is_same_v>); + // As with owning just, probing accepts this result, but calling transform triggers a static_assert. + constexpr auto fnXvalue = [](int &i) -> int && { return std::move(i); }; + static_assert(can_transform); + } + + SECTION("noexcept") + { + static_assert(noexcept(a.transform([](int &) noexcept { return 1; }))); + static_assert(not noexcept(a.transform([](int &) { return 1; }))); + SUCCEED(); + } + + SECTION("constexpr") + { + static_assert([] { + int x = 3; + R r{x}; + return &r.transform([](int &i) -> int & { return i; }).value() == &x + && std::move(r).transform([](int &i) { return i + 1; }).value() == 4; + }()); + SUCCEED(); + } + } + + SECTION("and_then") + { + int x = 3; + R a{x}; + // the callable receives int& for every category of the carrier and returns any just + auto r1 = std::move(a).and_then([](int &i) { return R{i}; }); + static_assert(std::is_same_v); + CHECK(&r1.value() == &x); + auto r2 = std::as_const(a).and_then([](int &i) { return fn::just{i != 0}; }); + static_assert(std::is_same_v>); + CHECK(r2.value()); + + constexpr auto fnRvalue = [](int &&) { return fn::just{1}; }; + static_assert(not can_and_then); + static_assert(can_and_then &&, decltype(fnRvalue)>); + static_assert(noexcept(a.and_then([](int &) noexcept { return fn::just{1}; }))); + static_assert(not noexcept(a.and_then([](int &) { return fn::just{1}; }))); + + SECTION("constexpr") + { + static_assert([] { + int x = 3; + return &R{x}.and_then([](int &i) { return R{i}; }).value() == &x; + }()); + SUCCEED(); + } + } + + SECTION("apply family") + { + int x = 3; + R a{x}; + CHECK(std::move(a).apply([](int &i, int y) { return i + y; }, 4) == 7); + CHECK(a.apply_r([](int &i) { return i; }) == 3L); + // the tag names the payload type, a reference + CHECK(std::move(a).apply_type([](std::in_place_type_t, int &i) { return -i; }) == -3); + CHECK(a.apply_type_r([](std::in_place_type_t, int &i, int y) { return i + y; }, 1) == 4L); + constexpr auto ownedTag = [](std::in_place_type_t, int i) { return i; }; + static_assert(not can_apply_type); + static_assert(can_apply_type &, decltype(ownedTag)>); + + // a tuple-like referent goes by elements + std::tuple t{1, 2}; + fn::just &> p{t}; + CHECK(p.apply([](int &l, int &r) { return l + r; }) == 3); + + // the result conversion is a constraint + constexpr auto fnInt = [](int &i) { return i; }; + constexpr auto tagInt = [](std::in_place_type_t, int &i) { return i; }; + static_assert(can_apply_r); + static_assert(not can_apply_r); + static_assert(can_apply_type_r); + static_assert(not can_apply_type_r); + + static_assert(noexcept(a.apply([](int &) noexcept { return 1; }))); + static_assert(not noexcept(a.apply([](int &) { return 1; }))); + static_assert(noexcept(a.apply_r([](int &) noexcept { return 1; }))); + static_assert(not noexcept(a.apply_r([](int &) { return 1; }))); + static_assert(noexcept(a.apply_type([](std::in_place_type_t, int &) noexcept { return 1; }))); + static_assert(not noexcept(a.apply_type([](std::in_place_type_t, int &) { return 1; }))); + static_assert(noexcept(a.apply_type_r([](std::in_place_type_t, int &) noexcept { return 1; }))); + static_assert(not noexcept(a.apply_type_r([](std::in_place_type_t, int &) { return 1; }))); + + SECTION("constexpr") + { + static_assert([] { + int x = 3; + R r{x}; + return std::move(r).apply([](int &i, int y) { return i + y; }, 4) == 7 + && r.apply_r([](int &i) { return i; }) == 3L + && r.apply_type([](std::in_place_type_t, int &i) { return -i; }) == -3 + && r.apply_type_r([](std::in_place_type_t, int &i, int y) { return i + y; }, 1) == 4L; + }()); + static_assert([] { + std::tuple t{1, 2}; + return fn::just &>{t}.apply([](int &l, int &r) { return l + r; }) == 3; + }()); + SUCCEED(); + } + } + + SECTION("equality") + { + // the referents compare, not the addresses + int x = 1; + int y = 1; + int z = 2; + CHECK(R{x} == R{y}); + CHECK(R{x} != R{z}); + CHECK(R{x} == fn::just{1L}); + CHECK(R{x} == 1); + static_assert([] { + int x = 1; + int y = 1; + return R{x} == R{y} && R{x} == fn::just{1L} && R{x} == 1 && R{x} != 2; + }()); + } + + SECTION("operators") + { + // the product and the sum the operators build hold a copy of the referent, a product with the + // unit just included + int x = 1; + R a{x}; + auto p = a & fn::just{2.0}; + static_assert(std::is_same_v>>); + auto d = a | fn::just{7}; + static_assert(std::is_same_v>); + auto ul = fn::just{} & a; + auto ur = a & fn::just{}; + static_assert(std::is_same_v>>); + static_assert(std::is_same_v>>); + x = 5; + CHECK(p.value().apply([](int i, double) { return i; }) == 1); + CHECK(d.value() == 1); + CHECK(ul.value().apply([](int i) { return i; }) == 1); + CHECK(ur.value().apply([](int i) { return i; }) == 1); + static_assert([] { + int x = 1; + auto p = R{x} & fn::just{2.0}; + auto d = R{x} | fn::just{7}; + auto ul = fn::just{} & R{x}; + auto ur = R{x} & fn::just{}; + x = 5; + constexpr auto first = [](int i, auto &&...) { return i; }; + return p.value().apply(first) == 1 && d.value() == 1 && ul.value().apply(first) == 1 + && ur.value().apply(first) == 1; + }()); + } +} + TEST_CASE("just of void", "[just]") { using V = fn::just; diff --git a/tests/fn/optional.cpp b/tests/fn/optional.cpp index 7fd981b7..df5ff94f 100644 --- a/tests/fn/optional.cpp +++ b/tests/fn/optional.cpp @@ -1677,3 +1677,21 @@ TEST_CASE("optional of empty copack", "[optional][copack]") } } } + +TEST_CASE("optional of reference", "[optional]") +{ + SECTION("referent") + { + // Pack and copack referents are refused, whatever their cv-qualification; other objects, + // tuple-like ones included, are not. + static_assert(fn::detail::_optional_referent); + static_assert(fn::detail::_optional_referent); + static_assert(fn::detail::_optional_referent>); + static_assert(not fn::detail::_optional_referent>); + static_assert(not fn::detail::_optional_referent const>); + static_assert(not fn::detail::_optional_referent>); + static_assert(not fn::detail::_optional_referent const>); + static_assert(not fn::detail::_optional_referent>); + SUCCEED(); + } +} diff --git a/tests/fn/pack.cpp b/tests/fn/pack.cpp index 21fcebee..cf07c7cc 100644 --- a/tests/fn/pack.cpp +++ b/tests/fn/pack.cpp @@ -299,6 +299,12 @@ TEST_CASE("pack", "[pack]") static_assert(not fn::detail::_is_valid_pack_element>); static_assert(not fn::detail::_is_valid_pack_element>); static_assert(not fn::detail::_is_valid_pack_element>); + // nor are references to them, while a reference to an opaque atom is an element + static_assert(fn::detail::_is_valid_pack_element &>); + static_assert(not fn::detail::_is_valid_pack_element &>); + static_assert(not fn::detail::_is_valid_pack_element const &>); + static_assert(not fn::detail::_is_valid_pack_element &>); + static_assert(not fn::detail::_is_valid_pack_element const &>); // witnesses that the permitted atoms instantiate static_assert(pack, int>::size == 2); diff --git a/tests/fn/transform.cpp b/tests/fn/transform.cpp index 6df3f071..fbe0a7d8 100644 --- a/tests/fn/transform.cpp +++ b/tests/fn/transform.cpp @@ -744,11 +744,26 @@ TEST_CASE("transform just", "[transform][just][choice][identity]") // an inadmissible payload answers: the member's gated result leaves no viable overload constexpr auto probe = [](auto &&v, auto &&fn) { return requires { FWD(v) | fn::transform(FWD(fn)); }; }; - static_assert(not probe(fn::just{3}, [](int &i) -> int & { return i; })); + static_assert(not probe(fn::just{3}, [](int) -> int && { throw 0; })); static_assert(probe(fn::just{3}, [](int i) { return i; })); + // An lvalue-reference result produces a non-owning carrier. + static constexpr fn::just cj{3}; + constexpr auto fnView = [](int const &i) -> int const & { return i; }; + static_assert(probe(cj, fnView)); + // An rvalue carrier rejects it: the result may refer into the expiring payload. A reference + // carrier passes its referent, which outlives the carrier. + static_assert(not probe(fn::just{3}, fnView)); + static_assert(not probe(std::move(cj), fnView)); + static constexpr int ci = 3; + static_assert(probe(fn::just{ci}, fnView)); // The pipeline rejects a copack reference result but accepts the corresponding value result. static constexpr fn::copack cu{U{}}; static_assert(not probe(fn::just{3}, [](int) -> fn::copack const & { return cu; })); + static_assert(not probe(fn::just{3}, [](int) -> fn::copack<> const & { throw 0; })); + // Likewise for a pack reference result. + static constexpr fn::pack cp{3}; + static_assert(not probe(cj, [](int) -> fn::pack const & { return cp; })); + static_assert(probe(cj, [](int) -> fn::pack { return cp; })); static_assert(probe(fn::just{3}, [](int) { return cu; })); SUCCEED(); }