Skip to content

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

Merged
Bronek merged 26 commits into
mainfrom
bronek/lvalue_copack
Oct 25, 2026
Merged

Bronek merged 26 commits into
mainfrom
bronek/lvalue_copack

Conversation

@Bronek

@Bronek Bronek commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Add lvalue-reference alternatives to copack and choice, preserve them through |, &, transform and the lifts, and reject bindings that could dangle.

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. Like pack<T&>, a copack returns T&, or T const& when the copack is const, regardless of 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.

Reject temporaries for reference alternatives. Like optional<T&> (#454), reference alternatives reject bindings to temporaries: copack<int const&>{42} and choice<int const&>{std::move(x)} do not compile. The converting constructors are deleted when the selected alternative would bind to a temporary, so the call fails rather than falling through to another overload. The in_place_type constructors, assignment from a value, emplace and the as_copack/as_choice lifts are constrained. When the compiler lacks the builtin (GCC 12, MSVC before Visual Studio 2026 18.6), the check relies on the emulation in pfn::reference_converts_from_temporary.

Use the payload type for | and &. 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>>. References to pack and copack are not valid alternatives, so they still contribute values, as they do in transform results. This also fixes & and | for const referents and includes the | fix from #437, which rebases onto this PR.

Breaking: result types change when an optional<T&> operand is used with | or &. For example, optional<int&> & just<void> now yields optional<pack<int&>>, as required by the type algebra.

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 are still stored as values.

Breaking: reference results that could refer into an argument whose lifetime ends with the call, such as an alternative of an rvalue copack, are now rejected instead of copied. Return by value instead.

Never fall back to another cv/ref overload. copack::transform, choice's transform and and_then, and transform and transform_error on fn::optional and fn::expected follow the approach introduced for pfn in #459: callback requirements are checked by assertions rather than constraints.

Previously, a callback that rejected rvalues removed the rvalue overload from consideration. The const & overload could then pass an lvalue referring into the temporary operand, allowing a reference result to dangle. As in the standard, expected remains constrained on constructing the unchanged side.

fn::applicable_transform and fn::applicable_transform_error now check the payload directly, like fn::applicable_and_then, instead of probing the members. This keeps pipe operations SFINAE-friendly.

Breaking:

  • a callback that does not accept the payload in the operand's own cv/ref category is now rejected instead of reaching the const & overload;
  • checking these members with such a callback in a requires-expression causes a hard error; use fn::applicable_transform and its siblings instead;
  • through |, a callback arm taking auto & no longer accepts the alternatives of a temporary operand; take auto const &, as examples/simple now does.

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 preserve lvalues as references and reject those they cannot reference instead of copying them. Pass an rvalue, e.g. std::move(x), to lift a value instead; an array or a function cannot be lifted by value. conjoin stores a leading scalar by value, just as & stores subsequent scalars.

Breaking:

  • pack{x} now copies instead of referring to x; as_pack(x) still refers to it.
  • as_copack(x) and as_choice(x) now refer to x instead of copying it; move the source to store a value, or use CTAD to copy it.
  • 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 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>.

Deprecate as_copack and as_choice. Name the type instead: copack<int&>{x} and choice<int&>{x} refer to x, copack{x} and choice{x} copy it, and just{c} deduces the choice over the alternatives of a copack c. A lift chooses between a reference and a value based on the argument's value category, which may not be apparent at the call site. Naming the type makes that choice explicit. as_pack remains until brace initialization of pack rejects temporaries for reference elements (#463).

Normalize a single conjoin argument; return a single disjoin argument by value. conjoin folds & starting from its unit. With one argument, it applies & to the unit and that argument: 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.

Other commits fix MSVC spellings of copack result types, equality's noexcept specification for reference alternatives, and relocation of reference alternatives by emplace. They also update the reference listings to match the headers. Compile-failure tests for fn live in tests/fn/reject/ and share the machinery introduced for pfn in #461.

TYPE_ALGEBRA.md and CHANGELOG.md document each change.

The final tree passes locally with gcc 16 and clang 21 under sanitizers, and in CI. Each commit in the fallback fix was also built and tested with gcc 16. Compiler Explorer checks using the amalgamated header covered the fallback fix, rejection of temporaries, and copack and choice as template arguments on MSVC 19.44, 19.51 and latest. Deprecation warnings were checked on MSVC 19.44 and latest. Other MSVC checks and AppleClang checks are covered by CI.

Related issue found during this work: #450, & on a const pack<int&> fails to compile.

Closes #448
Closes #449
Part of #447: the reference alternatives of copack and choice.

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: This PR lets copack hold lvalue-reference alternatives. That makes choice<T&> and expected<copack<T&>, E> work, and references now survive |, &, transform and the lifts. Deduction (CTAD) now always deduces values. The PR contains several breaking changes.

Changes:

  • Reference alternatives: variadic_union stores a T& alternative as a pointer (_ref_holder). Assigning to it rebinds the reference, comparison compares the referred-to values, and a const copack hands out T const&. A value picks its alternative the way overload resolution picks among functions taking each one (_selected_alternative_t). The converting constructor is deleted when it would bind a reference to a temporary.
  • Algebra: in | and &, an optional<T&> operand now contributes T& (_payload_t). | places each value into its alternative by tag (_disj_tag_t).
  • transform: over a copack, an lvalue-reference result stays a reference alternative. A static_assert (_admissible_results) refuses a reference into an argument that expires with the call.
  • Monadic members: transform on copack, choice, optional and expected asserts its requirements instead of constraining on them. It no longer falls back to the const& overload for a temporary operand. The pipeline concepts get the answer from the payload (_copack_transformable).
  • Deduction and lifts: CTAD for pack, copack, just and choice strips references and cv-qualifiers, and no longer turns arrays or functions into pointers. as_copack and as_choice keep an lvalue as a reference and are now deprecated.
  • Folds: conjoin with one argument combines it with the unit of its kind, and disjoin with one argument returns it by value. This fixes dangling references to temporaries.
  • Tests and docs: adds compile-failure (reject) tests under tests/fn/reject/, shared test utilities (refusing.hpp, allow_deprecated.hpp), and updates TYPE_ALGEBRA.md, CHANGELOG.md and the reference docs.

Technical Notes: Whether a reference would bind to a temporary is decided by pfn::reference_converts_from_temporary. On compilers without the builtin it uses an emulation, so it can miss some bindings there.

🤖 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
Comment thread include/fn/detail/variadic_union.hpp
Bronek added 12 commits October 24, 2026 14:48
Assisted-by: Claude:claude-opus-5-5
Closes: #448
Assisted-by: Claude:claude-opus-5-5
…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
@Bronek
Bronek force-pushed the bronek/lvalue_copack branch from 0c71a59 to b4d75d5 Compare October 24, 2026 13:49
Move the refusing callback to tests/util and define add_reject_tests
ahead of both suites, so fn tests can declare compile failures as pfn
tests do.

Assisted-by: Claude:claude-opus-5-5
The gates probed the member transform of a copack payload or a choice.
When the rvalue overload dropped out, the const & overload answered
instead: a temporary operand passed for a callback refusing an rvalue,
which then received an lvalue into the temporary, and a reference result
into a temporary passed only to fail on instantiation. The gates now ask
what copack::transform requires, in the operand's own cv/ref category.
examples/simple relied on the fallback for its auto & arms.

Assisted-by: Claude:claude-opus-5-5
fn::optional::transform and fn::expected's transform and transform_error
lose their trailing decltype, which removed an overload whose callback
did not accept the payload in that category and let const & take a
temporary operand. expected keeps the draft's constructibility
constraints as requires-clauses, so a copy-only error still falls
through to const && as in pfn. Checked on MSVC 19.44, 19.51 and latest
through the amalgamated header on Compiler Explorer.

Assisted-by: Claude:claude-opus-5-5
copack::transform and choice's transform and and_then take the same
shape, so a callback refusing rvalues can no longer receive an lvalue
into a temporary copack or choice and return a reference into it. This
comes after the optional and expected change: with their trailing
decltype gone, no declaration names these deduced bodies, which MSVC
would instantiate for a non-viable overload.

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

The disjunction tests threw only from optional's left value and expected's
error fold, so the exception edge of copying optional's right value, and
either of expected's values, into the sum was never taken; codecov/patch
counted the lines missed.

Assisted-by: Claude:claude-opus-5-5
A reference alternative bound to a temporary dangled at once:
copack<int const&>{42} compiled. As optional<T&> does (#454, the C++26
rule), the converting constructors of copack and choice are deleted where
the alternative the value selects would bind to a temporary, so overload
resolution never routes such a value to another overload. The holder of a
reference alternative refuses the binding itself, which constrains away the
in_place_type constructors, assignment from a value, emplace and the lifts.

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

Bronek commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

augment review

@augmentcode

augmentcode Bot commented Oct 25, 2026

Copy link
Copy Markdown

This pull request is abnormally large and would use a significant amount of tokens to review. If you still wish to review it, comment "augment review" and we will review it.

@Bronek

Bronek commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

augment review

@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. 3 suggestions posted.

Fix All in Augment

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

Comment thread include/fn/copack.hpp
Comment thread include/fn/copack.hpp Outdated
Comment thread tests/fn/expected.cpp Outdated
Assisted-by: Claude:claude-opus-5-5
A lift picks a reference or a copy from the argument's value category,
which the call site does not show; the named type shows it, and its
constructors refuse a temporary for a reference alternative. `as_pack`
stays until `pack`'s braces refuse a temporary too (#463).

Assisted-by: Claude:claude-opus-5-5
Assisted-by: Claude:claude-opus-5-5
The referents sit at namespace scope: MSVC 19.44, 19.51 and latest hit
an internal compiler error reading through a class-type template
argument which points to a block-scope static, with or without libfn.

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

Bronek commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

augment review

@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/algebra.hpp
Comment thread tests/fn/just.cpp Outdated
Keying `|` on the payload type made `copack<A, B>&` or `pack<A>&` an
alternative, which no copack admits: `optional<copack_for<A, B>&> |
optional<C>` became a hard error where main flattened it into
`optional<copack_for<A, B, C>>`. The sum now takes a payload as it takes
a transform result, so such a reference enters as its value, while its
reference elements and alternatives stay references.

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

Bronek commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

augment review

@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. 1 suggestion posted.

Fix All in Augment

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

Comment thread tests/fn/copack.cpp Outdated
Assisted-by: Claude:claude-opus-5-5
@Bronek

Bronek commented Oct 25, 2026

Copy link
Copy Markdown
Member Author

augment review

@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. No suggestions at this time.

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

@sonarqubecloud

Copy link
Copy Markdown

@Bronek
Bronek merged commit 6912624 into main Oct 25, 2026
59 checks passed
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