diff --git a/CHANGELOG.md b/CHANGELOG.md index f66f0a73..f64d5cc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 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. +## `&` takes a `void` side as the unit factor `pack<>` — 27 September 2026 + +`&` treats a `void` value as the unit factor `pack<>`. For example, `expected & just` yields `expected, E>`; previously it yielded `expected`. This breaking change applies across carrier pairings in either order. Copack values distribute into packs, and `optional & just` yields an owning `optional>`. Two `void` values still yield `void`. + +## `pack::append` builds its result in place — 27 September 2026 + +`pack::append` constructs the final pack directly. Initializing its base from a temporary could otherwise relocate elements again and terminate if a move threw inside a `noexcept` call. Bracing each aggregate layer also prevents an element's conversion operator from initializing a whole layer in place of the element. + ## `transform` over a copack maps a `void` result to `pack<>` — 27 September 2026 `transform` over a copack represents `void` callback results as `pack<>`. Mixed `void` and `int` results yield `copack_for, int>`; all-`void` results yield `copack>`. This applies through `expected`, `optional` and `choice`, and to `transform_error` over a copack error. Plain error types still reject `void` results. Mutable operands can select a `void` overload where they previously fell back to a valued `const` overload. diff --git a/TYPE_ALGEBRA.md b/TYPE_ALGEBRA.md index eca36afa..854a0fd3 100644 --- a/TYPE_ALGEBRA.md +++ b/TYPE_ALGEBRA.md @@ -152,8 +152,6 @@ In `libfn`'s algebra, zero and unit are strictly separated: Because `pack<>` exists, applying a callable to it invokes a nullary function. Because `copack<>` is uninhabited, providing a callback over `copack<>` is statically proven to be unreachable code (dead code). -In C++, `void` is often conflated with empty state, but algebraically, `void` is a unit type `1`, similar to `pack<>`. - Consider the difference in these carrier states: | Computation | Meaning | @@ -223,6 +221,14 @@ To invoke the algebra, you use the opt-in mechanisms provided by the library: If a side is already a `copack` or `pack`, forwarding it behaves naturally without nesting. A `copack` on the error side of `expected` enables error-set unioning. Because monadic operations introduce no grade themselves, `and_then` widens a graded error side but rejects a differing ungraded one. Section 9 gives the exact promotion rules. +### The role of `void` + +`void` represents success without a payload in `just` and `expected`. Algebraically, this is the unit `1`, like `pack<>`. Neither `pack` nor `copack` can hold `void` directly. + +When an operation forms a `copack`, it represents a `void` result as `pack<>`. Joining only `void` branch values stays `void`; mixing them with other values introduces a `pack<>` alternative. Mapping a copack side always yields a copack, so even all-`void` callbacks produce `copack>`. + +In a conjunction, a `void` value contributes the unit factor `pack<>`. The value stays `void` only when both operands are `void`. On a plain side, `void` is valid as the value of `expected` or `just`, but not as the value of `optional` or the error of `expected`. Direct construction does not perform this conversion: `copack_for` is ill-formed. + ## 3. The computation carriers To model computation and manage control flow (success, failure, alternatives, and empty states), `libfn` uses **computation carriers** (often called "monadic types"). The three carrier templates are `optional`, `expected` and `just`. The list below groups them by fallibility and payload; `choice` names the copack specialization of `just`: @@ -401,7 +407,7 @@ Key principles of mapping: - `transform` preserves the carrier family; its member form never leaves its own carrier type. - Success and error states are preserved. - A bare `copack` has a member `transform` to map across alternatives, but takes no pipeline functor as it is data, not a carrier. -- Heterogeneous results inside `transform` or `transform_error` form a normalized `copack`. +- Over a `copack` side, `transform` and `transform_error` yield a normalized `copack`, with `void` results represented as `pack<>`. Over a plain side, only `expected` and `just` accept a `void` value result. - Applying `transform_error` to a carrier with no error side (such as `just` or `choice`) is ill-formed. - When a side is uninhabited (`copack<>`), transformation is well-formed but vacuous: neither the member nor the pipeline form is reachable, and the callback is not instantiated. This applies to `optional>` and `expected, E>` on the value side, and `expected>` on the error side. @@ -471,7 +477,7 @@ An operand from the identity cluster (Section 10) contributes a value but never - **Unchanged Errors**: Because identity cluster operands never fail, they add no alternatives to the error channel. A `just` or `choice` operand preserves the fallible operand's error side (plain or graded). An `expected>` operand contributes its uninhabited grade to the error union: no active alternative is added, although the resulting error channel is promoted to a graded copack. - **Value Bundling**: The identity cluster operand's value conjoins with the fallible operand's value into a `pack`. -- **Unit Elision**: `just` and `expected>` act as the product's identity unit and are elided from the value product (e.g., `expected & just` remains `expected`). +- **Unit Factor**: a `void` operand, such as `just` or `expected>`, contributes the unit `pack<>` to the value product: `expected & just` is `expected, E>`, as `expected & just>` is. The value stays `void` only when every operand is `void`. - **Choice Distribution**: Conjoining a `choice` with a fallible carrier distributes the coproduct through the product, yielding a `copack` of `pack`s wrapped in the fallible carrier. @@ -482,9 +488,8 @@ auto test_conjunction_with_identity_cluster(fn::expected ex, fn::jus auto res1 = ex & j; static_assert(std::same_as, Error>>); - // Conjoining with a unit (just) completely elides the unit auto res2 = ex & fn::just{}; - static_assert(std::same_as); + static_assert(std::same_as, Error>>); // Conjoining a choice causes distribution inside the carrier fn::choice ch = 1.5; @@ -507,7 +512,7 @@ auto test_conjunction_with_identity_cluster(fn::expected ex, fn::jus > ## 7. Sum composition with operator| (disjunction) -Disjunction evaluates alternative computations, keeping the first successful result. `a | b` fails only if both operands fail: dual to conjunction, their values union into a `copack`, and their errors combine into a `pack`. If both operands share the same value type, the value side remains ungraded. A `void` operand enters the sum as `pack<>`. +Disjunction evaluates alternative computations, keeping the first successful result. `a | b` fails only if both operands fail: dual to conjunction, their values union into a `copack`, and their errors combine into a `pack`. If both operands share the same value type, the value side remains ungraded, so two `void` operands stay `void`. Otherwise a `void` operand enters the sum as `pack<>`. If either error side is graded, the product distributes over it: $(E_1 + E_2) \times F \to (E_1 \times F) + (E_2 \times F)$ (the full Cartesian product when both are graded), yielding a canonical `copack` of `pack`s. @@ -590,7 +595,7 @@ Because member `.and_then` cannot change carrier families (Section 3), its *Klei - Copack-graded `expected` unions heterogeneous error sets. - Copack-valued inputs join heterogeneous successful branch types into a normalized `copack`. - Branch convergence preserves the exact type without duplicate union states. -- All-`void` branches join to `void`; mixed void and non-void branches are ill-formed. +- All-`void` branches join to `void`; in a mix of `void` and non-`void` branches, a `void` one joins as `pack<>`. - Callback results returning bare values require `transform` rather than `and_then`. The library formalizes this "same-kind" contract via the `fn::same_kind` concept, which lets generic templates probe whether two carrier types belong to the same monadic family: @@ -648,14 +653,14 @@ Value joining and error grading are independent: branch values can join while th During sequential composition, `libfn` derives the promoted type from the `copack` you supply: - In `and_then` (success binding), a plain error type `E` is promoted to `copack` if the returning **error type** of the callback is `copack`. -- In `or_else` (recovery/error binding), a plain success type `T` is promoted to `copack` if the returning **success type** of the callback is `copack`. +- In `or_else` (recovery/error binding), a plain success type `T` is promoted to `copack` if the returning **success type** of the callback is `copack`; for `void` that lift is `copack>`. An un-graded computation thus enters a graded pipeline without manual lifting. If you need to perform this promotion explicitly on the carrier itself before entering a composition, `libfn` provides direct member helpers: - `.copack_error()` on `expected` explicitly lifts the error, transforming `expected` to `expected>`. -- `.copack_value()` on `expected` explicitly lifts the success value, transforming `expected` to `expected, E>`. +- `.copack_value()` on `expected` explicitly lifts the success value, transforming `expected` to `expected, E>`, and `expected` to `expected>, E>`. - `.copack_value()` on `optional` symmetrically lifts the value, transforming `optional` to `optional>`. These helper methods provide a compact, explicit alternative to the pipeline promotions: @@ -1113,7 +1118,7 @@ Index: * The term **identity cluster** is introduced in Section 3 under the infallible carriers as a simple grouping definition: "_Together, `just`, `choice` and `expected>` form the identity cluster_." It does not expand on its operations or mathematical properties here, keeping the introduction minimal. **2. Core Algebraic Foundations (Sections 2, 3, & 4)** - * **Section 2** establishes the core algebraic identities of types (**0**, **1**, **A + B**, **A × B**), separates Zero (`copack<>`) from Unit (`pack<>`), and defines `copack` set semantics (deduplication, flattening, sorting) and why the algebra is strictly opt-in. + * **Section 2** establishes the core algebraic identities of types (**0**, **1**, **A + B**, **A × B**), separates Zero (`copack<>`) from Unit (`pack<>`), and defines `copack` set semantics (deduplication, flattening, sorting), why the algebra is strictly opt-in, and how `void` enters it. * **Section 3** establishes the carriers, the fact that raw data lacks control flow while carriers have it, and the basics of cross-carrier recovery-path bridging. * **Section 4** expands on the concrete C++ payload behaviors of `pack` and `copack` (ADL `get`, structured bindings, `.append()`, singular copacks). diff --git a/docs/reference/conjoin.md b/docs/reference/conjoin.md index ae774fde..665a855e 100644 --- a/docs/reference/conjoin.md +++ b/docs/reference/conjoin.md @@ -42,38 +42,31 @@ template constexpr auto operator&(Lh &&lh, Rh &&rh); // (3) template -constexpr auto operator&(Lh &&lh, Rh &&rh) -> expected::value_type, typename std::remove_cvref_t::error_type>; // (4) +constexpr auto operator&(Lh &&, Rh &&rh); // (4) template -constexpr auto operator&(Lh &&lh, Rh &&rh) -> expected::value_type, typename std::remove_cvref_t::error_type>; // (5) - -template -constexpr auto operator&(Lh &&, Rh &&rh); // (6) - -template -constexpr auto operator&(Lh &&lh, Rh &&); // (7) +constexpr auto operator&(Lh &&lh, Rh &&); // (5) template -constexpr auto operator&(Lh &&lh, Rh &&rh); // (8) -constexpr auto operator&(Lh &&, Rh &&rh) -> std::remove_cvref_t; // (9) -constexpr auto operator&(Lh &&lh, Rh &&) -> std::remove_cvref_t; // (10) +constexpr auto operator&(Lh &&lh, Rh &&rh); // (6) +constexpr auto operator&(Lh &&, Rh &&) -> just; // (7) template -constexpr auto operator&(Lh &&lh, Rh &&rh); // (11) +constexpr auto operator&(Lh &&lh, Rh &&rh); // (8) template -constexpr auto operator&(Lh &&lh, Rh &&rh); // (12) +constexpr auto operator&(Lh &&lh, Rh &&rh); // (9) template -constexpr auto operator&(Lh &&lh, Rh &&rh); // (13) +constexpr auto operator&(Lh &&lh, Rh &&rh); // (10) template -constexpr auto operator&(Lh &&, Rh &&rh); // (14) +constexpr auto operator&(Lh &&lh, Rh &&rh); // (11) template -constexpr auto operator&(Lh &&lh, Rh &&); // (15) +constexpr auto operator&(Lh &&lh, Rh &&rh); // (12) -constexpr auto operator&(auto &&lh, auto &&rh); // (16) +constexpr auto operator&(auto &&lh, auto &&rh); // (13) ``` :include-doxygen-doc: fn::operator& { args: "Lh &&, Rh &&" } diff --git a/examples/type_algebra/main.cpp b/examples/type_algebra/main.cpp index 88427db4..bc533620 100644 --- a/examples/type_algebra/main.cpp +++ b/examples/type_algebra/main.cpp @@ -217,9 +217,8 @@ auto test_conjunction_with_identity_cluster(fn::expected ex, fn::jus auto res1 = ex & j; static_assert(std::same_as, Error>>); - // Conjoining with a unit (just) completely elides the unit auto res2 = ex & fn::just{}; - static_assert(std::same_as); + static_assert(std::same_as, Error>>); // Conjoining a choice causes distribution inside the carrier fn::choice ch = 1.5; diff --git a/include/fn/detail/pack_impl.hpp b/include/fn/detail/pack_impl.hpp index dd661fab..7083464f 100644 --- a/include/fn/detail/pack_impl.hpp +++ b/include/fn/detail/pack_impl.hpp @@ -50,10 +50,8 @@ concept _makeable_element = requires { _make_element(::std::declval().. template concept _nothrow_makeable_element = requires { requires noexcept(_make_element(::std::declval()...)); }; -// One element's relocation into a new pack: the copy-initialization its holder performs -// ([dcl.init.aggr]/4.3, reached through brace elision), asked of the holder one element at a time - -// `_element{src}` elides into the same member copy-initialization. This excludes explicit -// constructors, where `is_[nothrow_]constructible_v` would admit them. +// Aggregate member copy-initialization excludes explicit constructors; +// is_constructible would admit them. template concept _relocatable_element = requires { E{::std::declval()}; }; @@ -81,13 +79,11 @@ struct _pack_append {}; template requires(not _some_pack) && (not _some_copack) struct _pack_append { - using impl = pack_impl<::std::index_sequence_for, Ts..., T>; using type = ::fn::pack; }; template struct _pack_append_pack; template struct _pack_append_pack<::fn::pack, Ts...> { - using impl = pack_impl<::std::index_sequence_for, Ts..., Tx...>; using type = ::fn::pack; }; // A tag can NAME a pack type whose own elements are invalid without instantiating it; splicing it @@ -99,7 +95,6 @@ struct _pack_append {}; template requires _some_pack && _spliceable_pack<::std::remove_cvref_t> struct _pack_append { - using impl = _pack_append_pack<::std::remove_cvref_t, Ts...>::impl; using type = _pack_append_pack<::std::remove_cvref_t, Ts...>::type; }; @@ -196,43 +191,45 @@ struct pack_impl<::std::index_sequence, Ts...> : _element... { static constexpr bool _nothrow_relocatable = (... && _nothrow_relocatable_element<_element, apply_const_lvalue_t>); - template + // Return the final pack: guaranteed elision does not apply to base subobjects. + // Brace each layer so element conversions cannot initialize a whole layer. + template , Ts..., T>, typename Self> static constexpr auto _append(Self &&self, auto &&...args) // - noexcept(_nothrow_relocatable && _nothrow_makeable_element) - -> pack_impl<::std::index_sequence, Ts..., T> + noexcept(_nothrow_relocatable && _nothrow_makeable_element) -> R requires(not _some_copack) && (not _some_pack) && _relocatable && _makeable_element { - return {static_cast>(FWD(self)._element::v)..., - _make_element(FWD(args)...)}; + if constexpr (_some_pack) + return {{{static_cast>(FWD(self)._element::v)}..., + {_make_element(FWD(args)...)}}}; + else + return {{static_cast>(FWD(self)._element::v)}..., + {_make_element(FWD(args)...)}}; } // Splicing is normalization, not elimination: the appended pack's elements relocate through // INVOKE, so a lone tuple-like element arrives whole instead of taking std::apply's meaning // through the engine's tuple arm. - template + template static constexpr auto _append(Self &&self, auto &&other) // noexcept(_nothrow_relocatable - && ::std::remove_cvref_t::_impl::template _nothrow_relocatable) -> // - typename _pack_append<::std::remove_cvref_t, Ts...>::impl + && ::std::remove_cvref_t::_impl::template _nothrow_relocatable) -> R requires _some_pack && (::std::is_same_v<::std::remove_cvref_t, ::std::remove_cvref_t>) && _relocatable && ::std::remove_cvref_t::_impl::template _relocatable { - using type = _pack_append<::std::remove_cvref_t, Ts...>::impl; - return FWD(other)._swap_invoke(FWD(other), [&self](auto &&...args) { - return type{static_cast>(FWD(self)._element::v)..., FWD(args)...}; + return FWD(other)._swap_invoke(FWD(other), [&](auto &&...args) -> R { + return {{{static_cast>(FWD(self)._element::v)}..., {FWD(args)}...}}; }); } // The tag form constructs the named pack from the arguments, then splices it: appending a pack // means concatenation in every spelling. One relocation per element more than appending the // elements directly - the spelling of an append is a performance knob, never a result change. - template + template static constexpr auto _append(Self &&self, auto &&...args) // noexcept(_nothrow_relocatable && _nothrow_initializable<::std::remove_cvref_t, decltype(args)...> - && ::std::remove_cvref_t::_impl::template _nothrow_relocatable<::std::remove_cvref_t>) -> // - typename _pack_append<::std::remove_cvref_t, Ts...>::impl + && ::std::remove_cvref_t::_impl::template _nothrow_relocatable<::std::remove_cvref_t>) -> R requires _some_pack && (not(sizeof...(args) == 1 && (... && ::std::is_same_v<::std::remove_cvref_t, ::std::remove_cvref_t>))) @@ -241,9 +238,8 @@ struct pack_impl<::std::index_sequence, Ts...> : _element... { _relocatable<::std::remove_cvref_t> { using pack_t = ::std::remove_cvref_t; - using type = _pack_append::impl; - return pack_t::_impl::_swap_invoke(pack_t{FWD(args)...}, [&self](auto &&...elems) { - return type{static_cast>(FWD(self)._element::v)..., FWD(elems)...}; + return pack_t::_impl::_swap_invoke(pack_t{FWD(args)...}, [&](auto &&...elems) -> R { + return {{{static_cast>(FWD(self)._element::v)}..., {FWD(elems)}...}}; }); } }; diff --git a/include/fn/expected.hpp b/include/fn/expected.hpp index 65f7ee48..2cd69724 100644 --- a/include/fn/expected.hpp +++ b/include/fn/expected.hpp @@ -1719,7 +1719,7 @@ template class expected : private detail::_expected_b * * As the primary, with the value side the unit: success-path callbacks are invoked with no * arguments, the `apply` family's value arm receives the trailing arguments alone, and a - * conjunction elides the void side from the value product. + * conjunction with a valued side takes the void side as the unit factor `pack<>`. * * @tparam Err Error type; a `copack` makes the carrier graded */ @@ -2346,7 +2346,7 @@ template class expected : private detail::_expected_ba * @brief The unit of the `expected` family: a carrier over `void` whose error side is uninhabited * * It always holds its empty value - `copack<>` offers no alternative to fail with - so it belongs to - * the identity cluster, and `operator&` elides it from a product. + * the identity cluster, and `operator&` with a valued side takes it as the unit factor `pack<>`. */ using expected_unit = expected>; @@ -2444,7 +2444,8 @@ template struct _expected_efn final { * @brief The conjunction of carriers: values multiply into a `pack`, errors sum into a `copack` * * `a & b` succeeds only where both operands do, the values folding into one `pack` - a `void` - * side elides, and a copack value distributes into a copack of packs. What the failure side + * side contributes the unit `pack<>`, and a copack value distributes into a copack of packs - + * except that two `void` sides stay `void`. What the failure side * carries depends on the carrier: an `expected` holds the leftmost failing operand's error, an * identical pair of error types staying as it is and any other pair summing into its normalized * `copack_for`, grading not required of the operands; an `optional` is simply empty, its unit @@ -2457,95 +2458,106 @@ template struct _expected_efn final { * @return The carrier of the folded value product, over the summed failure side */ template - requires some_expected_void && (not some_expected_void) - && ::std::is_same_v::error_type, - typename ::std::remove_cvref_t::error_type> + requires(not some_expected_void) && (not some_expected_void) + && ::std::is_same_v::error_type, + typename ::std::remove_cvref_t::error_type> [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // - noexcept(detail::_nothrow_join_expected< - expected::value_type, typename ::std::remove_cvref_t::error_type>, - Lh, Rh, decltype(FWD(rh).value())>) + noexcept(noexcept(::fn::detail::_join< + detail::template _expected_type::error_type>::template type>( + FWD(lh), FWD(rh), detail::_expected_efn::error_type>{}))) { using error_type = ::std::remove_cvref_t::error_type; - using value_type = ::std::remove_cvref_t::value_type; - using type = expected; - if (lh.has_value() && rh.has_value()) - return type{::std::in_place, FWD(rh).value()}; - else if (not lh.has_value()) - return type{::fn::unexpect, FWD(lh).error()}; - else - return type{::fn::unexpect, FWD(rh).error()}; + return ::fn::detail::_join::template type>( + FWD(lh), FWD(rh), detail::_expected_efn{}); } template - requires some_expected_void && (not some_expected_void) - && (not ::std::is_same_v::error_type, - typename ::std::remove_cvref_t::error_type>) + requires(not some_expected_void) && (not some_expected_void) + && (not ::std::is_same_v::error_type, + typename ::std::remove_cvref_t::error_type>) [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // - noexcept(detail::_nothrow_join_widened< - expected::value_type, - copack_for::error_type, - typename ::std::remove_cvref_t::error_type>>, - copack_for::error_type, typename ::std::remove_cvref_t::error_type>, - Lh, Rh, decltype(FWD(rh).value())>) + noexcept(noexcept( + ::fn::detail::_join< + detail::template _expected_type::error_type, + typename ::std::remove_cvref_t::error_type>>::template type>( + FWD(lh), FWD(rh), + detail::_expected_efn::error_type, + typename ::std::remove_cvref_t::error_type>>{}))) { using new_error_type = copack_for::error_type, typename ::std::remove_cvref_t::error_type>; - using value_type = ::std::remove_cvref_t::value_type; - using type = expected; - if (lh.has_value() && rh.has_value()) - return type{::std::in_place, FWD(rh).value()}; - else if (not lh.has_value()) { - if constexpr (not ::std::is_same_v::error_type, copack<>>) - return type{::fn::unexpect, new_error_type{FWD(lh).error()}}; - else - ::pfn::unreachable(); // LCOV_EXCL_LINE - } else { - if constexpr (not ::std::is_same_v::error_type, copack<>>) - return type{::fn::unexpect, new_error_type{FWD(rh).error()}}; - else - ::pfn::unreachable(); // LCOV_EXCL_LINE - } + return ::fn::detail::_join::template type>( + FWD(lh), FWD(rh), detail::_expected_efn{}); } +namespace detail { +// An uninhabited value cannot be folded; only error construction can throw. +template constexpr inline bool _nothrow_unit_fold = true; +template +constexpr inline bool _nothrow_unit_fold + = noexcept(::fn::detail::_fold_detail::fold<_factor_t, _factor_t>(::std::declval<_factor_of_t>(), + ::std::declval<_factor_of_t>())) + && _nothrow_initializable>; +} // namespace detail + template - requires(not some_expected_void) && some_expected_void + requires(some_expected_void != some_expected_void) && ::std::is_same_v::error_type, typename ::std::remove_cvref_t::error_type> [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // - noexcept(detail::_nothrow_join_expected< - expected::value_type, typename ::std::remove_cvref_t::error_type>, - Lh, Rh, decltype(FWD(lh).value())>) + noexcept(detail::_nothrow_unit_fold< + ::fn::detail::_uninhabited_join, + expected<::fn::detail::_joined_t, typename ::std::remove_cvref_t::error_type>, Lh, Rh> + && ::fn::detail::_nothrow_initializable< + expected<::fn::detail::_joined_t, typename ::std::remove_cvref_t::error_type>, + ::fn::unexpect_t, decltype(FWD(lh).error())> + && ::fn::detail::_nothrow_initializable< + expected<::fn::detail::_joined_t, typename ::std::remove_cvref_t::error_type>, + ::fn::unexpect_t, decltype(FWD(rh).error())>) { - using error_type = ::std::remove_cvref_t::error_type; - using value_type = ::std::remove_cvref_t::value_type; - using type = expected; - if (lh.has_value() && rh.has_value()) - return type{::std::in_place, FWD(lh).value()}; - else if (not lh.has_value()) + using type = expected<::fn::detail::_joined_t, typename ::std::remove_cvref_t::error_type>; + if constexpr (not ::fn::detail::_uninhabited_join) { + if (lh.has_value() && rh.has_value()) + return type{::std::in_place, + ::fn::detail::_fold_detail::fold<::fn::detail::_factor_t, ::fn::detail::_factor_t>( + ::fn::detail::_factor(FWD(lh)), ::fn::detail::_factor(FWD(rh)))}; + } + if (not lh.has_value()) return type{::fn::unexpect, FWD(lh).error()}; - else - return type{::fn::unexpect, FWD(rh).error()}; + return type{::fn::unexpect, FWD(rh).error()}; } template - requires(not some_expected_void) && some_expected_void + requires(some_expected_void != some_expected_void) && (not ::std::is_same_v::error_type, typename ::std::remove_cvref_t::error_type>) [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // - noexcept(detail::_nothrow_join_widened< - expected::value_type, - copack_for::error_type, - typename ::std::remove_cvref_t::error_type>>, - copack_for::error_type, typename ::std::remove_cvref_t::error_type>, - Lh, Rh, decltype(FWD(lh).value())>) + noexcept( + detail::_nothrow_unit_fold< + ::fn::detail::_uninhabited_join, + expected<::fn::detail::_joined_t, copack_for::error_type, + typename ::std::remove_cvref_t::error_type>>, + Lh, Rh> + && detail::_nothrow_error_lift::error_type, + typename ::std::remove_cvref_t::error_type>> + && detail::_nothrow_error_lift::error_type, + typename ::std::remove_cvref_t::error_type>> + && ::fn::detail::_nothrow_initializable< + expected<::fn::detail::_joined_t, copack_for::error_type, + typename ::std::remove_cvref_t::error_type>>, + ::fn::unexpect_t, + copack_for::error_type, typename ::std::remove_cvref_t::error_type>>) { using new_error_type = copack_for::error_type, typename ::std::remove_cvref_t::error_type>; - using value_type = ::std::remove_cvref_t::value_type; - using type = expected; - if (lh.has_value() && rh.has_value()) - return type{::std::in_place, FWD(lh).value()}; - else if (not lh.has_value()) { + using type = expected<::fn::detail::_joined_t, new_error_type>; + if constexpr (not ::fn::detail::_uninhabited_join) { + if (lh.has_value() && rh.has_value()) + return type{::std::in_place, + ::fn::detail::_fold_detail::fold<::fn::detail::_factor_t, ::fn::detail::_factor_t>( + ::fn::detail::_factor(FWD(lh)), ::fn::detail::_factor(FWD(rh)))}; + } + if (not lh.has_value()) { if constexpr (not ::std::is_same_v::error_type, copack<>>) return type{::fn::unexpect, new_error_type{FWD(lh).error()}}; else @@ -2604,41 +2616,6 @@ template } } -// Overloads when both sides are non-void, producing either of -// expected, ...> or expected, pack...>, ...> -template - requires(not some_expected_void) && (not some_expected_void) - && ::std::is_same_v::error_type, - typename ::std::remove_cvref_t::error_type> -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // - noexcept(noexcept(::fn::detail::_join< - detail::template _expected_type::error_type>::template type>( - FWD(lh), FWD(rh), detail::_expected_efn::error_type>{}))) -{ - using error_type = ::std::remove_cvref_t::error_type; - return ::fn::detail::_join::template type>( - FWD(lh), FWD(rh), detail::_expected_efn{}); -} - -template - requires(not some_expected_void) && (not some_expected_void) - && (not ::std::is_same_v::error_type, - typename ::std::remove_cvref_t::error_type>) -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // - noexcept(noexcept( - ::fn::detail::_join< - detail::template _expected_type::error_type, - typename ::std::remove_cvref_t::error_type>>::template type>( - FWD(lh), FWD(rh), - detail::_expected_efn::error_type, - typename ::std::remove_cvref_t::error_type>>{}))) -{ - using new_error_type - = copack_for::error_type, typename ::std::remove_cvref_t::error_type>; - return ::fn::detail::_join::template type>( - FWD(lh), FWD(rh), detail::_expected_efn{}); -} - namespace detail { // The cluster conjunction's specification: the cluster operand always contributes its value, so // only the expected operand's channels weigh - its error relocating unchanged into the result. @@ -2647,21 +2624,16 @@ template struct _nothrow_amp_cluster { static constexpr bool value - = noexcept(::fn::detail::_fold_detail::fold::value_type, - typename ::std::remove_cvref_t::value_type>( - ::std::declval<::fn::detail::_value_of_t>(), ::std::declval<::fn::detail::_value_of_t>())) + = noexcept(::fn::detail::_fold_detail::fold<::fn::detail::_factor_t, ::fn::detail::_factor_t>( + ::std::declval<::fn::detail::_factor_of_t>(), ::std::declval<::fn::detail::_factor_of_t>())) && _nothrow_initializable> && _nothrow_initializable; }; } // namespace detail -// The identity cluster in the conjunction: a just or choice operand always contributes its value -// to the product and adds no term to the error sum - the expected operand's error passes through -// unchanged, plain or graded, and its state alone decides. just is the product's unit and -// elides. template - requires(::fn::detail::_some_just) && (not ::std::is_void_v::value_type>) - && (not some_expected_void) + requires(::fn::detail::_some_just) + && (not(::std::is_void_v::value_type> && some_expected_void)) [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // noexcept(detail::_nothrow_amp_cluster< ::fn::detail::_uninhabited_join, @@ -2672,17 +2644,18 @@ template if constexpr (::fn::detail::_uninhabited_join) { return type{::fn::unexpect, FWD(rh).error()}; } else { - using VL = ::std::remove_cvref_t::value_type; - using VR = ::std::remove_cvref_t::value_type; + using VL = ::fn::detail::_factor_t; + using VR = ::fn::detail::_factor_t; if (rh.has_value()) - return type{::std::in_place, ::fn::detail::_fold_detail::fold(FWD(lh).value(), FWD(rh).value())}; + return type{::std::in_place, ::fn::detail::_fold_detail::fold(::fn::detail::_factor(FWD(lh)), + ::fn::detail::_factor(FWD(rh)))}; return type{::fn::unexpect, FWD(rh).error()}; } } template - requires(::fn::detail::_some_just) && (not ::std::is_void_v::value_type>) - && (not some_expected_void) + requires(::fn::detail::_some_just) + && (not(some_expected_void && ::std::is_void_v::value_type>)) [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // noexcept(detail::_nothrow_amp_cluster< ::fn::detail::_uninhabited_join, @@ -2693,51 +2666,16 @@ template if constexpr (::fn::detail::_uninhabited_join) { return type{::fn::unexpect, FWD(lh).error()}; } else { - using VL = ::std::remove_cvref_t::value_type; - using VR = ::std::remove_cvref_t::value_type; + using VL = ::fn::detail::_factor_t; + using VR = ::fn::detail::_factor_t; if (lh.has_value()) - return type{::std::in_place, ::fn::detail::_fold_detail::fold(FWD(lh).value(), FWD(rh).value())}; + return type{::std::in_place, ::fn::detail::_fold_detail::fold(::fn::detail::_factor(FWD(lh)), + ::fn::detail::_factor(FWD(rh)))}; return type{::fn::unexpect, FWD(lh).error()}; } } template - requires(::fn::detail::_some_just) && (not ::std::is_void_v::value_type>) -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // - noexcept( - ::fn::detail::_nothrow_initializable< - expected::value_type, typename ::std::remove_cvref_t::error_type>, - ::std::in_place_t, decltype(FWD(lh).value())> - && ::fn::detail::_nothrow_initializable< - expected::value_type, typename ::std::remove_cvref_t::error_type>, - ::fn::unexpect_t, decltype(FWD(rh).error())>) - -> expected::value_type, typename ::std::remove_cvref_t::error_type> -{ - using type = expected::value_type, typename ::std::remove_cvref_t::error_type>; - if (rh.has_value()) - return type{::std::in_place, FWD(lh).value()}; - return type{::fn::unexpect, FWD(rh).error()}; -} - -template - requires(::fn::detail::_some_just) && (not ::std::is_void_v::value_type>) -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // - noexcept( - ::fn::detail::_nothrow_initializable< - expected::value_type, typename ::std::remove_cvref_t::error_type>, - ::std::in_place_t, decltype(FWD(rh).value())> - && ::fn::detail::_nothrow_initializable< - expected::value_type, typename ::std::remove_cvref_t::error_type>, - ::fn::unexpect_t, decltype(FWD(lh).error())>) - -> expected::value_type, typename ::std::remove_cvref_t::error_type> -{ - using type = expected::value_type, typename ::std::remove_cvref_t::error_type>; - if (lh.has_value()) - return type{::std::in_place, FWD(rh).value()}; - return type{::fn::unexpect, FWD(lh).error()}; -} - -template requires ::fn::detail::_some_just && ::std::is_void_v::value_type> [[nodiscard]] constexpr auto operator&(Lh &&, Rh &&rh) // noexcept(::fn::detail::_nothrow_initializable<::std::remove_cvref_t, Rh>) -> ::std::remove_cvref_t @@ -2745,7 +2683,7 @@ template return ::std::remove_cvref_t{FWD(rh)}; } -template +template requires ::fn::detail::_some_just && ::std::is_void_v::value_type> [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&) // noexcept(::fn::detail::_nothrow_initializable<::std::remove_cvref_t, Lh>) -> ::std::remove_cvref_t diff --git a/include/fn/just.hpp b/include/fn/just.hpp index decfa977..1e640e32 100644 --- a/include/fn/just.hpp +++ b/include/fn/just.hpp @@ -1365,47 +1365,34 @@ template namespace detail { template -using _just_fold_t = decltype(_fold_detail::fold::value_type, - typename ::std::remove_cvref_t::value_type>( - ::std::declval().value(), ::std::declval().value())); +using _just_fold_t = decltype(_fold_detail::fold<_factor_t, _factor_t>(::std::declval<_factor_of_t>(), + ::std::declval<_factor_of_t>())); template constexpr inline bool _nothrow_just_fold - = noexcept(_fold_detail::fold::value_type, - typename ::std::remove_cvref_t::value_type>(::std::declval().value(), - ::std::declval().value())) + = noexcept(_fold_detail::fold<_factor_t, _factor_t>(::std::declval<_factor_of_t>(), + ::std::declval<_factor_of_t>())) && ::std::is_nothrow_constructible_v>, _just_fold_t>; } // namespace detail -// The conjunction inside the cluster: just & just folds the payloads and stays just, and -// just is the product's unit - it elides, whatever the other operand. template requires detail::_some_just && detail::_some_just - && (not ::std::is_void_v::value_type>) - && (not ::std::is_void_v::value_type>) + && (not(::std::is_void_v::value_type> + && ::std::is_void_v::value_type>)) [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // noexcept(detail::_nothrow_just_fold) { - using VL = ::std::remove_cvref_t::value_type; - using VR = ::std::remove_cvref_t::value_type; - return just>{::fn::detail::_fold_detail::fold(FWD(lh).value(), FWD(rh).value())}; + using VL = detail::_factor_t; + using VR = detail::_factor_t; + return just>{ + ::fn::detail::_fold_detail::fold(detail::_factor(FWD(lh)), detail::_factor(FWD(rh)))}; } template requires detail::_some_just && ::std::is_void_v::value_type> - && detail::_some_just -[[nodiscard]] constexpr auto operator&(Lh &&, Rh &&rh) // - noexcept(::std::is_nothrow_constructible_v<::std::remove_cvref_t, Rh>) -> ::std::remove_cvref_t -{ - return ::std::remove_cvref_t{FWD(rh)}; -} - -template - requires detail::_some_just && (not ::std::is_void_v::value_type>) && detail::_some_just && ::std::is_void_v::value_type> -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&) // - noexcept(::std::is_nothrow_constructible_v<::std::remove_cvref_t, Lh>) -> ::std::remove_cvref_t +[[nodiscard]] constexpr auto operator&(Lh &&, Rh &&) noexcept -> just { - return ::std::remove_cvref_t{FWD(lh)}; + return {}; } namespace detail { diff --git a/include/fn/optional.hpp b/include/fn/optional.hpp index 764bf025..98bbc429 100644 --- a/include/fn/optional.hpp +++ b/include/fn/optional.hpp @@ -1673,9 +1673,8 @@ template // The identity cluster in the conjunction: a just or choice operand always contributes its value // to the product and adds no term to the error sum, so the optional operand's state decides alone. -// just is the product's unit and elides. template - requires(::fn::detail::_some_just) && (not ::std::is_void_v::value_type>) + requires ::fn::detail::_some_just [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // noexcept(::fn::detail::_nothrow_join) { @@ -1683,16 +1682,17 @@ template if constexpr (::fn::detail::_uninhabited_join) { return type{::std::nullopt}; } else { - using VL = ::std::remove_cvref_t::value_type; - using VR = ::std::remove_cvref_t::value_type; + using VL = ::fn::detail::_factor_t; + using VR = ::fn::detail::_factor_t; if (rh.has_value()) - return type{::std::in_place, ::fn::detail::_fold_detail::fold(FWD(lh).value(), FWD(rh).value())}; + return type{::std::in_place, ::fn::detail::_fold_detail::fold(::fn::detail::_factor(FWD(lh)), + ::fn::detail::_factor(FWD(rh)))}; return type{::std::nullopt}; } } template - requires(::fn::detail::_some_just) && (not ::std::is_void_v::value_type>) + requires ::fn::detail::_some_just [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // noexcept(::fn::detail::_nothrow_join) { @@ -1700,36 +1700,17 @@ template if constexpr (::fn::detail::_uninhabited_join) { return type{::std::nullopt}; } else { - using VL = ::std::remove_cvref_t::value_type; - using VR = ::std::remove_cvref_t::value_type; + using VL = ::fn::detail::_factor_t; + using VR = ::fn::detail::_factor_t; if (lh.has_value()) - return type{::std::in_place, ::fn::detail::_fold_detail::fold(FWD(lh).value(), FWD(rh).value())}; + return type{::std::in_place, ::fn::detail::_fold_detail::fold(::fn::detail::_factor(FWD(lh)), + ::fn::detail::_factor(FWD(rh)))}; return type{::std::nullopt}; } } -template - requires ::fn::detail::_some_just && ::std::is_void_v::value_type> -[[nodiscard]] constexpr auto operator&(Lh &&, Rh &&rh) // - noexcept(::fn::detail::_nothrow_initializable<::std::remove_cvref_t, Rh>) -> ::std::remove_cvref_t -{ - return ::std::remove_cvref_t{FWD(rh)}; -} - -template - requires ::fn::detail::_some_just && ::std::is_void_v::value_type> -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&) // - noexcept(::fn::detail::_nothrow_initializable<::std::remove_cvref_t, Lh>) -> ::std::remove_cvref_t -{ - return ::std::remove_cvref_t{FWD(lh)}; -} - -// The identity expected is the cluster's third member: its uninhabited error contributes nothing -// to the sum, so against optional it composes exactly as just does - and expected> -// elides as the product's unit. template requires ::fn::detail::_some_expected && empty_copack::error_type> - && (not ::std::is_void_v::value_type>) [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // noexcept(::fn::detail::_nothrow_join) { @@ -1737,17 +1718,17 @@ template if constexpr (::fn::detail::_uninhabited_join) { return type{::std::nullopt}; } else { - using VL = ::std::remove_cvref_t::value_type; - using VR = ::std::remove_cvref_t::value_type; + using VL = ::fn::detail::_factor_t; + using VR = ::fn::detail::_factor_t; if (rh.has_value()) - return type{::std::in_place, ::fn::detail::_fold_detail::fold(FWD(lh).value(), FWD(rh).value())}; + return type{::std::in_place, ::fn::detail::_fold_detail::fold(::fn::detail::_factor(FWD(lh)), + ::fn::detail::_factor(FWD(rh)))}; return type{::std::nullopt}; } } template requires ::fn::detail::_some_expected && empty_copack::error_type> - && (not ::std::is_void_v::value_type>) [[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&rh) // noexcept(::fn::detail::_nothrow_join) { @@ -1755,32 +1736,15 @@ template if constexpr (::fn::detail::_uninhabited_join) { return type{::std::nullopt}; } else { - using VL = ::std::remove_cvref_t::value_type; - using VR = ::std::remove_cvref_t::value_type; + using VL = ::fn::detail::_factor_t; + using VR = ::fn::detail::_factor_t; if (lh.has_value()) - return type{::std::in_place, ::fn::detail::_fold_detail::fold(FWD(lh).value(), FWD(rh).value())}; + return type{::std::in_place, ::fn::detail::_fold_detail::fold(::fn::detail::_factor(FWD(lh)), + ::fn::detail::_factor(FWD(rh)))}; return type{::std::nullopt}; } } -template - requires ::fn::detail::_some_expected && empty_copack::error_type> - && ::std::is_void_v::value_type> -[[nodiscard]] constexpr auto operator&(Lh &&, Rh &&rh) // - noexcept(::fn::detail::_nothrow_initializable<::std::remove_cvref_t, Rh>) -> ::std::remove_cvref_t -{ - return ::std::remove_cvref_t{FWD(rh)}; -} - -template - requires ::fn::detail::_some_expected && empty_copack::error_type> - && ::std::is_void_v::value_type> -[[nodiscard]] constexpr auto operator&(Lh &&lh, Rh &&) // - noexcept(::fn::detail::_nothrow_initializable<::std::remove_cvref_t, Lh>) -> ::std::remove_cvref_t -{ - return ::std::remove_cvref_t{FWD(lh)}; -} - // The disjunction of optionals: values sum into a `copack`, empty only when both are // // `a | b` holds the leftmost engaged operand's value, injected into the sum of the value types - diff --git a/include/fn/pack.hpp b/include/fn/pack.hpp index ac251391..a08161d6 100644 --- a/include/fn/pack.hpp +++ b/include/fn/pack.hpp @@ -89,34 +89,38 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto append(::std::in_place_type_t, auto &&...args) & // - noexcept(noexcept(_impl::template _append(::std::declval(), FWD(args)...))) -> append_type - requires requires { append_type{_impl::template _append(*this, FWD(args)...)}; } + noexcept(noexcept(_impl::template _append>(::std::declval(), FWD(args)...))) + -> append_type + requires requires { _impl::template _append>(*this, FWD(args)...); } { - return {_impl::template _append(*this, FWD(args)...)}; + return _impl::template _append>(*this, FWD(args)...); } template [[nodiscard]] constexpr auto append(::std::in_place_type_t, auto &&...args) const & // - noexcept(noexcept(_impl::template _append(::std::declval(), FWD(args)...))) -> append_type - requires requires { append_type{_impl::template _append(*this, FWD(args)...)}; } + noexcept(noexcept(_impl::template _append>(::std::declval(), FWD(args)...))) + -> append_type + requires requires { _impl::template _append>(*this, FWD(args)...); } { - return {_impl::template _append(*this, FWD(args)...)}; + return _impl::template _append>(*this, FWD(args)...); } template [[nodiscard]] constexpr auto append(::std::in_place_type_t, auto &&...args) && // - noexcept(noexcept(_impl::template _append(::std::declval(), FWD(args)...))) -> append_type - requires requires { append_type{_impl::template _append(::std::move(*this), FWD(args)...)}; } + noexcept(noexcept(_impl::template _append>(::std::declval(), FWD(args)...))) + -> append_type + requires requires { _impl::template _append>(::std::move(*this), FWD(args)...); } { - return {_impl::template _append(::std::move(*this), FWD(args)...)}; + return _impl::template _append>(::std::move(*this), FWD(args)...); } template [[nodiscard]] constexpr auto append(::std::in_place_type_t, auto &&...args) const && // - noexcept(noexcept(_impl::template _append(::std::declval(), FWD(args)...))) -> append_type - requires requires { append_type{_impl::template _append(::std::move(*this), FWD(args)...)}; } + noexcept(noexcept(_impl::template _append>(::std::declval(), FWD(args)...))) + -> append_type + requires requires { _impl::template _append>(::std::move(*this), FWD(args)...); } { - return {_impl::template _append(::std::move(*this), FWD(args)...)}; + return _impl::template _append>(::std::move(*this), FWD(args)...); } /** @@ -130,38 +134,42 @@ template struct pack : detail::pack_impl<::std::index_sequence_ */ template [[nodiscard]] constexpr auto append(Arg &&arg) & // - noexcept(noexcept(_impl::template _append(::std::declval(), FWD(arg)))) -> append_type + noexcept(noexcept(_impl::template _append>(::std::declval(), FWD(arg)))) + -> append_type requires(not some_in_place_type) - && requires { append_type{_impl::template _append(*this, FWD(arg))}; } + && requires { _impl::template _append>(*this, FWD(arg)); } { - return {_impl::template _append(*this, FWD(arg))}; + return _impl::template _append>(*this, FWD(arg)); } template [[nodiscard]] constexpr auto append(Arg &&arg) const & // - noexcept(noexcept(_impl::template _append(::std::declval(), FWD(arg)))) -> append_type + noexcept(noexcept(_impl::template _append>(::std::declval(), FWD(arg)))) + -> append_type requires(not some_in_place_type) - && requires { append_type{_impl::template _append(*this, FWD(arg))}; } + && requires { _impl::template _append>(*this, FWD(arg)); } { - return {_impl::template _append(*this, FWD(arg))}; + return _impl::template _append>(*this, FWD(arg)); } template [[nodiscard]] constexpr auto append(Arg &&arg) && // - noexcept(noexcept(_impl::template _append(::std::declval(), FWD(arg)))) -> append_type + noexcept(noexcept(_impl::template _append>(::std::declval(), FWD(arg)))) + -> append_type requires(not some_in_place_type) - && requires { append_type{_impl::template _append(::std::move(*this), FWD(arg))}; } + && requires { _impl::template _append>(::std::move(*this), FWD(arg)); } { - return {_impl::template _append(::std::move(*this), FWD(arg))}; + return _impl::template _append>(::std::move(*this), FWD(arg)); } template [[nodiscard]] constexpr auto append(Arg &&arg) const && // - noexcept(noexcept(_impl::template _append(::std::declval(), FWD(arg)))) -> append_type + noexcept(noexcept(_impl::template _append>(::std::declval(), FWD(arg)))) + -> append_type requires(not some_in_place_type) - && requires { append_type{_impl::template _append(::std::move(*this), FWD(arg))}; } + && requires { _impl::template _append>(::std::move(*this), FWD(arg)); } { - return {_impl::template _append(::std::move(*this), FWD(arg))}; + return _impl::template _append>(::std::move(*this), FWD(arg)); } /** @@ -331,6 +339,21 @@ namespace detail { // the accessor spelled as a type rather than as a call which would drag its own throw in. template using _value_of_t = decltype(::std::declval().value()); +template +using _factor_t = ::std::conditional_t<::std::is_void_v::value_type>, + ::fn::pack<>, typename ::std::remove_cvref_t::value_type>; +template +using _factor_of_t = ::std::conditional_t<::std::is_void_v::value_type>, + ::fn::pack<>, _value_of_t>; + +template [[nodiscard]] constexpr auto _factor(Monad &&m) -> _factor_of_t +{ + if constexpr (::std::is_void_v::value_type>) + return {}; + else + return FWD(m).value(); +} + // A product with an uninhabited factor is itself uninhabited, and copack<> has no value fold - the // join over such a side must not name the fold, in the declared type, the noexcept specification // or the body, and always resolves through `efn`. @@ -342,9 +365,8 @@ template struct _joined { using type = copack<>; }; template struct _joined { - using type = decltype(::fn::detail::_fold_detail::fold::value_type, - typename ::std::remove_cvref_t::value_type>( - ::std::declval<_value_of_t>(), ::std::declval<_value_of_t>())); + using type = decltype(::fn::detail::_fold_detail::fold<_factor_t, _factor_t>( + ::std::declval<_factor_of_t>(), ::std::declval<_factor_of_t>())); }; template using _joined_t = typename _joined<_uninhabited_join, Lh, Rh>::type; @@ -358,14 +380,12 @@ struct _nothrow_join_arm { }; template