Skip to content

Allow lvalue-reference alternatives in copack; keep references through |, &, transform and lifts (breaking) - #451

Open
Bronek wants to merge 13 commits into
mainfrom
bronek/lvalue_copack
Open

Bronek wants to merge 13 commits into
mainfrom
bronek/lvalue_copack

Conversation

@Bronek

@Bronek Bronek commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Five commits:

1. Allow lvalue-reference alternatives in copack. An alternative may be T&, held as a pointer to its referent; T, T& and T const& are distinct alternatives. As with optional<T&>, assignment rebinds and comparison compares referents. As through a pack<T&>, a copack hands out T&, or T const& when const, whatever its value category. A value selects among the alternatives of its own decayed type by overload resolution, so copack_for<int, int&>{x} is ambiguous; std::in_place_type<int&> names the alternative. choice<T&> and expected<copack<T&>, E> now work, too.

2. Key | and & on the payload type. An optional<T&> operand contributes T& rather than T: optional<int&> | optional<long&> is optional<copack_for<int&, long&>>, and optional<int&> & optional<long> is optional<pack<int&, long>>. A const referent no longer leaks into & or breaks |. This absorbs the | fix of #437, which rebases onto it.

Breaking: result types change wherever an optional<T&> operand meets | or &, including optional<int&> & just<void>, now optional<pack<int&>> — this brings the intended behaviour of the type algebra.

3. Keep lvalue-reference results of transform over a copack. A callback returning T& yields a reference alternative, as optional's transform yields optional<T&>; other reference results still enter as values.

Breaking: a reference result that could refer into an argument expiring with the call, such as an alternative of an rvalue copack, is refused where it was copied; return by value instead.

4. Deduce values and lift references. CTAD for pack, copack, just and choice removes only references and cv-qualifiers: for an lvalue x, pack{x} is pack<int>, and pack{"abc"} is pack<char[4]>. The lifts as_pack, as_copack and as_choice keep an lvalue as a reference, and refuse one they cannot refer to rather than copy it. Pass an rvalue, e.g. std::move(x), to lift a value instead; an array or a function cannot be lifted by value. conjoin holds a leading scalar by value, as & holds the others.

Breaking:

  • pack{x} copies where it referred; as_pack(x) still refers.
  • as_copack(x) and as_choice(x) refer where they copied; move the source, or use CTAD, to copy.
  • for an array arr and a function f, just{arr}, just{f} and pack{f} fail to compile, as do as_copack of an lvalue pack or array and as_choice of an lvalue copack, array or function, such as as_choice("hi").
  • for int x, y;, conjoin(x, y) is pack<int, int>, where it was pack<int&, int>.

5. Normalize a single conjoin argument; return a single disjoin argument by value. conjoin is the fold of & from its unit, so a single argument meets the unit alone: conjoin(x) is pack<int>, a copack_for<A, B> becomes copack_for<pack<A>, pack<B>>, and an optional<int> becomes optional<pack<int>>, while a void value stays void. disjoin(m) returns m by value.

This fixes a dangling reference. With one argument, both returned the argument itself as a reference, so binding the result of a call on a temporary left a reference to a destroyed object. Both now return a value, which the binding keeps alive:

auto &&r = fn::conjoin(42);                         // pack<int>; was int&& to a destroyed temporary
auto &&p = fn::conjoin(fn::pack{1, 2});             // pack<int, int>; was pack<int, int>&&, likewise
auto const &d = fn::disjoin(fn::optional<int>{4});  // optional<int>; was optional<int>&&, likewise

Breaking: the result of a single-argument conjoin changes type, e.g. conjoin(o).value() is a pack<int> where it was an int; callbacks still receive the unpacked elements.

TYPE_ALGEBRA.md and CHANGELOG.md document each change.

Each commit was tested locally with gcc 16 and clang 22, and the final tree in the CI images for gcc 12–15 and clang 19 and 20; MSVC and AppleClang are covered by CI only.

Found along the way: #450, & on a const pack<int&> fails to compile.

Closes #448
Closes #449

Assisted-by: Claude:claude-opus-5-5

Bronek added 4 commits October 4, 2026 21:53
Assisted-by: Claude:claude-opus-5-5
Closes: #448
Assisted-by: Claude:claude-opus-5-5
@codecov

codecov Bot commented Oct 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@augmentcode

augmentcode Bot commented Oct 4, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: Preserve borrowed lvalues throughout libfn's type algebra.

  • Permit object lvalue-reference alternatives in copack, stored as pointers.
  • Expose these through choice and copack-valued expected.
  • Rebind reference assignments and compare referents.
  • Make | and & compose payload types, including optional<T&>.
  • Retain admissible lvalue-reference results from copack transforms.
  • Add checks against borrowing from expiring transform arguments.
  • Make CTAD remove cv/ref without pointer decay; lifts preserve lvalue references.
  • Hold leading conjoin scalars by value.
Compatibility: Breaking deduction/result-type changes are documented, with expanded runtime and constexpr tests.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread include/fn/copack.hpp
[[nodiscard]] constexpr auto _collapsing_apply(Data &&data, ::std::size_t index, Fn &&fn, Args &&...args) //
noexcept(_is_nothrow_rts_applicable<To, _copack_injector<To, Fn &&>, Data &&, Args &&...>) -> To
{
static_assert(_admissible_results<Fn &&, Data &&, ::std::remove_cvref_t<Data>, Args &&...>,

@augmentcode augmentcode Bot Oct 4, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you check the const& overload fallback at include/fn/copack.hpp:240? A projector with operator()(int const&) -> int const& and deleted operator()(int const&&) makes both rvalue transform overloads non-viable, so copack<int>{1}.transform(projector) falls back to const&, passes this assertion as an lvalue, and returns a reference to the destroyed temporary instead of rejecting it.

Severity: high

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread include/fn/detail/variadic_union.hpp
Bronek added 9 commits October 4, 2026 23:28
…ment by value

A single argument was forwarded as a reference, which dangled when bound to a
temporary and gave a result whose shape depended on the number of arguments.

Assisted-by: Claude:claude-opus-5-5
MSVC instantiates the deduced-return body of a non-viable rvalue transform
overload while forming its declaration, which fired the reference-result
guard for a const lvalue call.

Assisted-by: Claude:claude-opus-5-5
Assisted-by: Claude:claude-opus-5-5
…the headers

Declare get_ptr with a trailing return, which doxygen reads as constexpr, and
list the single-carrier conjoin overload first, so that each listing has the
shape the signature check derives from the headers.

Assisted-by: Claude:claude-opus-5-5
@sonarqubecloud

sonarqubecloud Bot commented Oct 5, 2026

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Should & keep lvalue-reference payloads of optional<T&>? Allow lvalue-reference alternatives in copack

1 participant