You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Lift the reference restriction at just.hpp:89 for lvalue references. just<T&> becomes well-formed and adopts the semantics of optional<T&> verbatim. Rvalue references remain ill-formed, matching optional and pack's rejection of T&& elements.
Motivation: Close the Identity Cluster
C++26 adopted optional<T&>. pfn polyfills it, and TYPE_ALGEBRA §15 already recognizes it as "the deliberate exception—the standard specifies it." However, §10 defines just<T> as the carrier canonically isomorphic to its payload: the always-engaged floor beneath optional<T>.
Because optional<T&> exists, the library now contains a reference-capable fallible carrier whose infallible floor cannot be expressed. There is no spelling for the degenerate, always-engaged case of optional<T&>. The standard's evolution created this asymmetry within the identity cluster; admitting just<T&> removes it. This is both an extension and a fix.
The Remaining Boundary Is Storage-Principled
Lvalue references are permitted where storage is direct: in pack elements, optional, and just. The payload of just is a plain member (just.hpp:102); no union is involved.
References remain prohibited where a union stores alternatives: in expected, copack alternatives, and therefore choice. C++ forbids union members of reference type, and std::expected draws exactly the same boundary.
The note in §15 changes from "the carriers refuse references, optional excepted" to this storage-based rule. The wrap-in-a-pack doctrine remains, but with a sharper purpose: it is how a reference crosses the union boundary, as in expected<pack<T&>, E>, rather than how references are carried in general.
Semantics
Every semantic question inherits a ruling already in force:
Assignment rebinds, never assigns through — mirroring optional<T&>.
Comparison compares referents, following the existing rule shared by optional<T&> and pack<T&>.
value() and callback invocation always yield T&, never T&&. An rvalue just<T&> must not synthesize a move from a referent it does not own, mirroring C++26 optional<T&>.
CTAD never deduces a reference. The guide just(T) -> just<T> at just.hpp:742 takes T by value. Consequently, just{lvalue} currently decays to just<int> and continues to do so; the reference form requires explicit spelling. This is the safety gate that makes every just<T&> deliberate.
There is no default constructor. A reference payload is born bound: the unit requires its argument, and there is no disengaged state to default to.
Compiled Witness
The probe below pins the cited optional<int&> precedent behaviors and the CTAD decay guarantee. It compiles cleanly today with g++ 16.2.1 and clang 22.1.8 under -std=c++20 -Wall -Wextra -Werror; every check is a static_assert over a constexpr evaluation.
constexprboolprobe_referent_compare_and_rebind()
{
int a = 1;
int b = 1;
fn::optional<int &> x{a};
fn::optional<int &> y{b};
boolconst referent_compare = (x == y); // distinct objects, equal values
x = fn::optional<int &>{b}; // assignment rebinds ...boolconst rebound = (&x.value() == &b) && a == 1; // ... leaving `a` untouchedreturn referent_compare && rebound;
}
static_assert(probe_referent_compare_and_rebind());
// value() yields int& regardless of the optional's own value categorystatic_assert(std::same_as<decltype(std::declval<fn::optional<int &> &>().value()), int &>);
static_assert(std::same_as<decltype(std::declval<fn::optional<int &> &&>().value()), int &>);
constexprboolprobe_ctad_decays()
{
int x = 42;
fn::just j{x};
static_assert(std::same_as<decltype(j), fn::just<int>>); // lvalue deduces int, never int&return j.value() == 42;
}
static_assert(probe_ctad_decays());
The full probe also establishes that transform on optional<int&> receives int& and leaves the referent untouched.
Consequences
Admission to transform and and_then widens: a callback returning U& produces just<U&>, where that return is refused today. This is strictly additive—previously ill-formed code becomes well-formed, while no existing program changes meaning.
The remaining refusals—expected<T&, E>, a reference copack alternative, and choice with a reference alternative—receive a diagnostic that explains the union rule and names pack<T&> as the crossing spelling. Without that explanation, "just<T&> is legal, but choice<T&, U> is illegal" appears arbitrary.
The interaction table, normalization, deduplication, and grading remain unchanged. References never enter a copack, so the sum machinery never sees them. Bridging through and_then between just<T&> and optional<T&> requires no new machinery; as always, bind adopts the callback's family.
Implementation requires a reference specialization of just storage: a pointer internally to support rebinding, with T& at the interface. Keep the type structural, add a value-category test battery as required by CONTRIBUTING, and apply the usual MSVC care around deduced returns. The absence of union involvement keeps the risk profile low.
The proposals are independent in substance. After the re-founding, just has two specializations, and T& belongs to the non-copack specialization. The copack specialization—the choice carrier—continues to reject references under the union rule. Coordinate the implementations, but either may land first.
Documentation
Rewrite the reference-payload note in §15 around the storage-principled boundary.
Note in §10 that the reference sub-cluster spans just<T&> and optional<T&>, ending at the union boundary; crossing that boundary is explicit through pack.
Add a dated CHANGELOG.md entry recording what this reverses and why.
List it among the documented changes in the 0.2.0 release notes.
Lift the reference restriction at
just.hpp:89for lvalue references.just<T&>becomes well-formed and adopts the semantics ofoptional<T&>verbatim. Rvalue references remain ill-formed, matchingoptionalandpack's rejection ofT&&elements.Motivation: Close the Identity Cluster
C++26 adopted
optional<T&>.pfnpolyfills it, and TYPE_ALGEBRA §15 already recognizes it as "the deliberate exception—the standard specifies it." However, §10 definesjust<T>as the carrier canonically isomorphic to its payload: the always-engaged floor beneathoptional<T>.Because
optional<T&>exists, the library now contains a reference-capable fallible carrier whose infallible floor cannot be expressed. There is no spelling for the degenerate, always-engaged case ofoptional<T&>. The standard's evolution created this asymmetry within the identity cluster; admittingjust<T&>removes it. This is both an extension and a fix.The Remaining Boundary Is Storage-Principled
Lvalue references are permitted where storage is direct: in
packelements,optional, andjust. The payload ofjustis a plain member (just.hpp:102); no union is involved.References remain prohibited where a union stores alternatives: in
expected, copack alternatives, and thereforechoice. C++ forbids union members of reference type, andstd::expecteddraws exactly the same boundary.The note in §15 changes from "the carriers refuse references,
optionalexcepted" to this storage-based rule. The wrap-in-a-packdoctrine remains, but with a sharper purpose: it is how a reference crosses the union boundary, as inexpected<pack<T&>, E>, rather than how references are carried in general.Semantics
Every semantic question inherits a ruling already in force:
optional<T&>.optional<T&>andpack<T&>.value()and callback invocation always yieldT&, neverT&&. An rvaluejust<T&>must not synthesize a move from a referent it does not own, mirroring C++26optional<T&>.just(T) -> just<T>atjust.hpp:742takesTby value. Consequently,just{lvalue}currently decays tojust<int>and continues to do so; the reference form requires explicit spelling. This is the safety gate that makes everyjust<T&>deliberate.Compiled Witness
The probe below pins the cited
optional<int&>precedent behaviors and the CTAD decay guarantee. It compiles cleanly today with g++ 16.2.1 and clang 22.1.8 under-std=c++20 -Wall -Wextra -Werror; every check is astatic_assertover aconstexprevaluation.The full probe also establishes that
transformonoptional<int&>receivesint&and leaves the referent untouched.Consequences
transformandand_thenwidens: a callback returningU&producesjust<U&>, where that return is refused today. This is strictly additive—previously ill-formed code becomes well-formed, while no existing program changes meaning.fn::thunk- call-by-name right operands for the carrier conjunction and disjunction #372 loses its sole exception.() -> T&lifts directly tojust<T&>, superseding thejust<pack<T&>>ruling recorded there. Amend Addfn::thunk- call-by-name right operands for the carrier conjunction and disjunction #372 if this proposal is accepted. The thunk lift then becomes fully uniform: carriers pass through, and everything else embeds asjust<result>.expected<T&, E>, a reference copack alternative, andchoicewith a reference alternative—receive a diagnostic that explains the union rule and namespack<T&>as the crossing spelling. Without that explanation, "just<T&>is legal, butchoice<T&, U>is illegal" appears arbitrary.and_thenbetweenjust<T&>andoptional<T&>requires no new machinery; as always, bind adopts the callback's family.juststorage: a pointer internally to support rebinding, withT&at the interface. Keep the type structural, add a value-category test battery as required by CONTRIBUTING, and apply the usual MSVC care around deduced returns. The absence of union involvement keeps the risk profile low.Interaction with #414
The proposals are independent in substance. After the re-founding,
justhas two specializations, andT&belongs to the non-copack specialization. The copack specialization—the choice carrier—continues to reject references under the union rule. Coordinate the implementations, but either may land first.Documentation
just<T&>andoptional<T&>, ending at the union boundary; crossing that boundary is explicit throughpack.Assisted-by: Claude:claude-fable-5