From 99ef3e96545cb41cebd144166985723716a3ba01 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Thu, 24 Sep 2026 21:54:10 +0100 Subject: [PATCH 1/5] Support lvalue-reference payloads in `just` Add `just` with rebinding assignment and emplace. Access preserves the lvalue reference regardless of the carrier's constness or value category. Transform can preserve supported reference results. Reject copack referents and joins of differing result types that include reference payloads. Document the lifetime limitations and cover reference construction, rebinding, callable operations, and joins in tests. Assisted-by: Claude:claude-opus-5-5 --- CHANGELOG.md | 15 ++ TYPE_ALGEBRA.md | 4 +- docs/reference/just.md | 119 ++++++++++++ include/fn/copack.hpp | 9 + include/fn/just.hpp | 232 ++++++++++++++++++++++- tests/fn/and_then.cpp | 49 +++++ tests/fn/just.cpp | 410 +++++++++++++++++++++++++++++++++++++++++ tests/fn/transform.cpp | 6 +- 8 files changed, 839 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20237d04..7579c2d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ 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. +## `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, 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 packs and 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. +- 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`. Eliding the unit `just` under `&` returns the other operand itself, so `just{} & just{x}` is still `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. + ## `choice` becomes `just` over a copack — 21 September 2026 `choice` is now an alias of `just>`. The specialization forwards operations on alternatives to its `copack` payload. The `` header is gone; `` declares `choice`, `choice_for` and `some_choice`. diff --git a/TYPE_ALGEBRA.md b/TYPE_ALGEBRA.md index 766121e9..ba12e34e 100644 --- a/TYPE_ALGEBRA.md +++ b/TYPE_ALGEBRA.md @@ -1035,14 +1035,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 packs and tuple-like referents as `fn::apply` does. `libfn` polyfills the standard's `optional`. `just` excludes references to copacks: dispatch would depend on an active alternative the carrier does not own. 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 5fa4dc4e..ba64bd3c 100644 --- a/include/fn/copack.hpp +++ b/include/fn/copack.hpp @@ -166,6 +166,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 @@ -177,6 +180,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 decfa977..20121694 100644 --- a/include/fn/just.hpp +++ b/include/fn/just.hpp @@ -43,13 +43,20 @@ template concept some_choice = detail::_some_choice; namespace detail { +// Supported referents for just. Copacks are excluded because dispatch would depend on +// the referent's active alternative, state the carrier does not own (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_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 @@ -83,6 +90,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>; @@ -694,6 +702,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 packs and + * 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, 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`, packs and 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/tests/fn/and_then.cpp b/tests/fn/and_then.cpp index 2f72e523..5498854c 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,52 @@ 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}; }}; + return &fn::choice_for{B{}}.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 1de2d097..7a95ab32 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,10 @@ 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()); // a copack result lands on the choice over its alternatives - the same carrier family constexpr auto fnCopack @@ -232,6 +260,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(); } } @@ -342,6 +375,383 @@ 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 &>); + // never a copack: dispatch would follow the referent's active alternative (issue #434) + 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 + 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>); + // ... while eliding the unit just returns the other operand itself, a reference included + static_assert(std::is_same_v{} & a), R>); + static_assert(std::is_same_v{}), R>); + CHECK(&(fn::just{} & a).value() == &x); + CHECK(&(a & fn::just{}).value() == &x); + x = 5; + CHECK(p.value().apply([](int i, double) { return i; }) == 1); + CHECK(d.value() == 1); + static_assert([] { + int x = 1; + auto p = R{x} & fn::just{2.0}; + auto d = R{x} | fn::just{7}; + x = 5; + return p.value().apply([](int i, double) { return i; }) == 1 && d.value() == 1 + && &(fn::just{} & R{x}).value() == &x && &(R{x} & fn::just{}).value() == &x; + }()); + } +} + TEST_CASE("just of void", "[just]") { using V = fn::just; diff --git a/tests/fn/transform.cpp b/tests/fn/transform.cpp index 758b2a5f..0a334822 100644 --- a/tests/fn/transform.cpp +++ b/tests/fn/transform.cpp @@ -744,11 +744,15 @@ 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}; + static_assert(probe(cj, [](int const &i) -> int const & { return i; })); // 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; })); static_assert(probe(fn::just{3}, [](int) { return cu; })); SUCCEED(); } From 762d268ff6ecc51af440b907f2cb96ee1509745e Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Fri, 25 Sep 2026 21:41:45 +0100 Subject: [PATCH 2/5] Workaround in tests for VS 2022 Assisted-by: Claude:claude-opus-5-5 --- tests/fn/and_then.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/fn/and_then.cpp b/tests/fn/and_then.cpp index 5498854c..d8871fe7 100644 --- a/tests/fn/and_then.cpp +++ b/tests/fn/and_then.cpp @@ -1101,7 +1101,9 @@ TEST_CASE("and_then across the identity cluster", "[and_then][just][choice][expe int v = 1; auto const fnView = fn::overload{[&v](A) { return fn::just{v}; }, // [&v](B) { return fn::just{v}; }}; - return &fn::choice_for{B{}}.and_then(fnView).value() == &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. From 246078ccecb2837cadcf53182eec6320d2c8cad4 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Fri, 25 Sep 2026 22:08:19 +0100 Subject: [PATCH 3/5] Reject reference transform results from rvalue owning `just` An rvalue owning `just` passes its payload to the callback as an rvalue, so a reference result may refer into a payload that expires with the carrier. `optional` accepts such results as C++26 specifies; `just` diverges deliberately. Assisted-by: Claude:claude-opus-5-5 --- CHANGELOG.md | 2 +- TYPE_ALGEBRA.md | 2 +- include/fn/just.hpp | 13 ++++++++++--- tests/fn/just.cpp | 7 +++++++ tests/fn/transform.cpp | 9 ++++++++- 5 files changed, 27 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7579c2d1..b95e4c56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ There is no default constructor. For an `int x`, `just{x}` still deduces `just`, referring to the returned object. +- 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`. Eliding the unit `just` under `&` returns the other operand itself, so `just{} & just{x}` is still `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. diff --git a/TYPE_ALGEBRA.md b/TYPE_ALGEBRA.md index ba12e34e..474810ee 100644 --- a/TYPE_ALGEBRA.md +++ b/TYPE_ALGEBRA.md @@ -1042,7 +1042,7 @@ The library respects C++ value mechanics: > > ### Note — reference payloads > -> 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 packs and tuple-like referents as `fn::apply` does. `libfn` polyfills the standard's `optional`. `just` excludes references to copacks: dispatch would depend on an active alternative the carrier does not own. 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 packs and tuple-like referents as `fn::apply` does. `libfn` polyfills the standard's `optional`. `just` excludes references to copacks: dispatch would depend on an active alternative the carrier does not own. 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/include/fn/just.hpp b/include/fn/just.hpp index 20121694..1d79fe82 100644 --- a/include/fn/just.hpp +++ b/include/fn/just.hpp @@ -59,10 +59,13 @@ concept _just_payload && (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 @@ -261,6 +264,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 */ diff --git a/tests/fn/just.cpp b/tests/fn/just.cpp index 7a95ab32..e6a9f975 100644 --- a/tests/fn/just.cpp +++ b/tests/fn/just.cpp @@ -239,6 +239,13 @@ TEST_CASE("just", "[just]") 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 diff --git a/tests/fn/transform.cpp b/tests/fn/transform.cpp index 0a334822..b7e6b6bc 100644 --- a/tests/fn/transform.cpp +++ b/tests/fn/transform.cpp @@ -748,7 +748,14 @@ TEST_CASE("transform just", "[transform][just][choice][identity]") 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}; - static_assert(probe(cj, [](int const &i) -> int const & { return i; })); + 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; })); From 0c7d109c01fdbd12d65e66908766bf2b99c64e02 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 3 Oct 2026 21:44:52 +0100 Subject: [PATCH 4/5] Refuse pack and copack referents in reference carriers How a reference to a product or a sum takes part in conjunction, disjunction and grading is an open question, so `just` and `fn::optional` refuse both until the type algebra answers it. Assisted-by: Claude:claude-opus-5-5 --- CHANGELOG.md | 8 ++++++-- TYPE_ALGEBRA.md | 2 +- include/fn/just.hpp | 17 +++++++++-------- include/fn/optional.hpp | 9 ++++++++- tests/fn/just.cpp | 7 ++++++- tests/fn/optional.cpp | 13 +++++++++++++ tests/fn/transform.cpp | 4 ++++ 7 files changed, 47 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46007418..2a815656 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,15 @@ 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. + ## `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, or copacks. Rvalue-reference payloads remain unsupported, as in `optional` and `pack`. +`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 packs and tuple-like referents as `fn::apply` does. `apply_type` tags the payload with `std::in_place_type`. +`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. diff --git a/TYPE_ALGEBRA.md b/TYPE_ALGEBRA.md index 265d1030..1b66e11a 100644 --- a/TYPE_ALGEBRA.md +++ b/TYPE_ALGEBRA.md @@ -1042,7 +1042,7 @@ The library respects C++ value mechanics: > > ### Note — reference payloads > -> 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 packs and tuple-like referents as `fn::apply` does. `libfn` polyfills the standard's `optional`. `just` excludes references to copacks: dispatch would depend on an active alternative the carrier does not own. 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>`). +> 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/include/fn/just.hpp b/include/fn/just.hpp index 1d79fe82..726de90b 100644 --- a/include/fn/just.hpp +++ b/include/fn/just.hpp @@ -43,11 +43,12 @@ template concept some_choice = detail::_some_choice; namespace detail { -// Supported referents for just. Copacks are excluded because dispatch would depend on -// the referent's active alternative, state the carrier does not own (issue #434). +// 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_copack); +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 @@ -714,12 +715,12 @@ template <> struct just { * * 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 packs and - * tuple-like referents as `fn::apply` does. + * 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, or copacks. The carrier is trivially copyable and a + * 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 @@ -850,7 +851,7 @@ template struct just { /** * @brief Eliminates the referent through the callable * - * As with `fn::apply`, packs and tuple-like referents are expanded into their elements; other + * 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 diff --git a/include/fn/optional.hpp b/include/fn/optional.hpp index 764bf025..56a8cd10 100644 --- a/include/fn/optional.hpp +++ b/include/fn/optional.hpp @@ -36,6 +36,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 { @@ -1071,7 +1076,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 */ @@ -1081,6 +1087,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/just.cpp b/tests/fn/just.cpp index e6a9f975..a13c29f1 100644 --- a/tests/fn/just.cpp +++ b/tests/fn/just.cpp @@ -471,7 +471,12 @@ TEST_CASE("just of reference", "[just]") static_assert(not fn::detail::_just_payload); static_assert(not fn::detail::_just_payload); static_assert(not fn::detail::_just_payload &>); - // never a copack: dispatch would follow the referent's active alternative (issue #434) + // 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 &>); diff --git a/tests/fn/optional.cpp b/tests/fn/optional.cpp index 4b9a5270..c51ba609 100644 --- a/tests/fn/optional.cpp +++ b/tests/fn/optional.cpp @@ -1685,3 +1685,16 @@ TEST_CASE("optional of empty copack", "[optional][copack]") } } } + +TEST_CASE("optional of reference referent", "[optional]") +{ + // Pack and copack referents are refused; owned packs and copacks are not. + 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/transform.cpp b/tests/fn/transform.cpp index b7e6b6bc..39f53c0a 100644 --- a/tests/fn/transform.cpp +++ b/tests/fn/transform.cpp @@ -760,6 +760,10 @@ TEST_CASE("transform just", "[transform][just][choice][identity]") 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(); } From a66ccbe09220d6207b83df93e43215bd55f3e346 Mon Sep 17 00:00:00 2001 From: Bronek Kozicki Date: Sat, 3 Oct 2026 21:53:22 +0100 Subject: [PATCH 5/5] Pin pack and copack references as refused elements Assisted-by: Claude:claude-opus-5-5 --- tests/fn/optional.cpp | 25 +++++++++++++++---------- tests/fn/pack.cpp | 6 ++++++ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/fn/optional.cpp b/tests/fn/optional.cpp index c51ba609..67aca45f 100644 --- a/tests/fn/optional.cpp +++ b/tests/fn/optional.cpp @@ -1686,15 +1686,20 @@ TEST_CASE("optional of empty copack", "[optional][copack]") } } -TEST_CASE("optional of reference referent", "[optional]") +TEST_CASE("optional of reference", "[optional]") { - // Pack and copack referents are refused; owned packs and copacks are not. - 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(); + 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 9ecf0906..6b295b65 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);