Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@

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.

## Reference holders refuse to bind a temporary — 11 October 2026

`pfn::optional<T&>` and `fn::optional<T&>` reject a construction or `emplace` that would bind the reference to a temporary, as C++26 specifies through `reference_constructs_from_temporary_v`. Previously the polyfill left this guard out, so such constructions compiled and dangled. As in the standard, an xvalue of the referenced type is refused too: `optional<int const &>{std::move(x)}` no longer compiles.

The same rule applies to a `pack` reference element constructed by `as_pack` with spelled element types, or by `append(std::in_place_type<T &>, ...)`. Brace initialization of a `pack` is aggregate initialization and stays outside the rule.

`pfn::invoke_r` is no longer viable where its result would bind a reference `R` to a temporary; libc++ previously accepted such a call. The new `pfn::is_invocable_r` and `pfn::is_nothrow_invocable_r` give this C++23 answer on every standard library.

`fn::apply_r` and the `is_applicable_r` traits ask those traits, so a reference result that would bind a temporary is not applicable, rather than a hard error.

The new `<pfn/type_traits.hpp>` provides `pfn::reference_constructs_from_temporary` and `pfn::reference_converts_from_temporary`. Where the compiler lacks the builtin (GCC 12, MSVC before Visual Studio 2026 18.6), they are emulated without false positives, but miss a temporary created by converting a class source not reference-related to the referenced type, or one bound by a reference to an array.

## `<fn/monadic.hpp>` becomes `<fn/traits.hpp>`; `monadic_invocable` rejects an incomplete functor — 27 September 2026

Replace includes of `<fn/monadic.hpp>` with `<fn/traits.hpp>`; no compatibility header is provided. `some_in_place_type` moves to `<fn/traits.hpp>` and remains available through `<fn/copack.hpp>`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ See [examples/](examples/) and the [API reference][docs] for more. [TYPE_ALGEBRA

The library has two layers:

* **`pfn`** (`include/pfn`, namespace `pfn`) provides C++20 polyfills for standard-library facilities through C++26: `expected`, `optional` (including monadic operations, `optional<T&>` and range support), `invoke_r` and `unreachable`. These follow the C++ standard and accepted proposals, including `has_error()`.
* **`pfn`** (`include/pfn`, namespace `pfn`) provides C++20 polyfills for standard-library facilities through C++26: `expected`, `optional` (including monadic operations, `optional<T&>` and range support), `invoke_r`, `unreachable`, and the `reference_constructs_from_temporary` and `reference_converts_from_temporary` traits. These follow the C++ standard and accepted proposals, including `has_error()`.
* **`fn`** (`include/fn`, namespace `fn`) builds on those polyfills. It adds combinators such as `and_then`, `transform` and `recover`, along with the vocabulary types `pack`, `copack` and `choice`.

The `fn` types extend their `pfn` counterparts: switching a valid program from `pfn` types to `fn` changes neither compilation nor program behaviour, while making the types and operations defined in `fn` available.
Expand Down
4 changes: 2 additions & 2 deletions TYPE_ALGEBRA.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ A `pack` stores multiple fields and supports the standard C++ tuple protocol (`g

To explicitly lift values into a `pack` (which is useful when conjoining scalars with other packs or copacks), use `fn::as_pack(...)`. When called without template parameters, `as_pack` is deduction-only and preserves the value category of its arguments: `as_pack(42)` yields `pack<int>`, whereas calling `as_pack(x)` on an lvalue `x` yields `pack<int&>` (a reference rather than a copy).

Spelling the template parameters instead (e.g., `as_pack<bool, int>(x, d)`) takes deduction out of the picture: each argument is passed as the type you named, enabling implicit conversions to happen at the call boundary. A reference element becomes something you ask for explicitly — `as_pack<int const&>(x)` yields `pack<int const&>`. Note that partial template spelling is not supported; all element types must be spelled out explicitly if template parameters are specified.
Spelling the template parameters instead (e.g., `as_pack<bool, int>(x, d)`) takes deduction out of the picture: each argument is passed as the type you named, enabling implicit conversions to happen at the call boundary. A reference element becomes something you ask for explicitly — `as_pack<int const&>(x)` yields `pack<int const&>` — and it binds only where no temporary is created: `as_pack<int const&>(42)` does not compile. Note that partial template spelling is not supported; all element types must be spelled out explicitly if template parameters are specified.

<!-- sync-example-test-pack -->
```cpp
Expand Down Expand Up @@ -1040,7 +1040,7 @@ 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<T&...>` and `optional<T&>` are supported. Lifetime management of non-owning references remains with the caller.
- Reference-bearing `pack<T&...>` and `optional<T&>` are supported. `optional<T&>`, `as_pack` with spelled element types, and `append(std::in_place_type<T&>, ...)` refuse to bind a reference to a temporary, as C++26 `optional<T&>` does; beyond that, lifetime management of non-owning references remains with the caller.
- `pack` compares element-wise, supporting equality and three-way comparison. For reference-bearing `pack<T&...>`, comparison applies to the referents rather than the references themselves.

> [!NOTE]
Expand Down
17 changes: 12 additions & 5 deletions docs/reference/optional.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,20 @@ template <class Arg>
constexpr explicit optional(std::in_place_t, Arg &&arg); // (4)

template <class U>
constexpr explicit optional(U &&u); // (5)
constexpr explicit optional(optional<U> &rhs); // (6)
constexpr explicit optional(optional<U> const &rhs); // (7)
constexpr explicit optional(optional<U> &&rhs); // (8)
constexpr explicit optional(optional<U> const &&rhs); // (9)
constexpr explicit optional(U &&u); // (5)
constexpr explicit optional(U &&u) = delete; // (6)
constexpr explicit optional(optional<U> &rhs); // (7)
constexpr explicit optional(optional<U> &rhs) = delete; // (8)
constexpr explicit optional(optional<U> const &rhs); // (9)
constexpr explicit optional(optional<U> const &rhs) = delete; // (10)
constexpr explicit optional(optional<U> &&rhs); // (11)
constexpr explicit optional(optional<U> &&rhs) = delete; // (12)
constexpr explicit optional(optional<U> const &&rhs); // (13)
constexpr explicit optional(optional<U> const &&rhs) = delete; // (14)
```

Each deleted overload is selected where the reference would bind to a temporary.

:include-doxygen-doc: fn::optional< T & >::optional { args: "" }

:include-doxygen-doc: fn::optional< T & >::optional { args: "::std::nullopt_t" }
Expand Down
8 changes: 4 additions & 4 deletions docs/reference/pack.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ template <typename... Explicit, typename T, typename... Args>
constexpr auto as_pack(T &&src, Args &&...args) -> pack<T, Args...>; // (2)

template <typename T, typename... Args>
constexpr auto as_pack(std::type_identity_t<T> src, std::type_identity_t<Args>... args) -> pack<T, Args...>; // (3)
constexpr auto as_pack(detail::_as_pack_arg_t<T> src, detail::_as_pack_arg_t<Args>... args) -> pack<T, Args...>; // (3)
```

:include-doxygen-doc: fn::as_pack { args: "" }
Expand All @@ -114,11 +114,11 @@ constexpr auto as_pack(std::type_identity_t<T> src, std::type_identity_t<Args>..

:include-doxygen-doc-params: fn::as_pack { args: "T &&, Args &&...", title: "parameters" }

:include-doxygen-doc: fn::as_pack { args: "::std::type_identity_t< T >, ::std::type_identity_t< Args >..." }
:include-doxygen-doc: fn::as_pack { args: "detail::_as_pack_arg_t< T >, detail::_as_pack_arg_t< Args >..." }

:include-doxygen-doc-params: fn::as_pack { args: "::std::type_identity_t< T >, ::std::type_identity_t< Args >...", type: "template", title: "template parameters" }
:include-doxygen-doc-params: fn::as_pack { args: "detail::_as_pack_arg_t< T >, detail::_as_pack_arg_t< Args >...", type: "template", title: "template parameters" }

:include-doxygen-doc-params: fn::as_pack { args: "::std::type_identity_t< T >, ::std::type_identity_t< Args >...", title: "parameters" }
:include-doxygen-doc-params: fn::as_pack { args: "detail::_as_pack_arg_t< T >, detail::_as_pack_arg_t< Args >...", title: "parameters" }

## get {style: "api"}

Expand Down
65 changes: 61 additions & 4 deletions docs/reference/pfn.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ nor behaviour.

`pfn` polyfills only what C++20 lacks: all of `<expected>`, the C++23 and C++26 additions to
`std::optional` (the monadic operations, iterator support, `optional<T&>`), `std::apply` in its
SFINAE-friendly C++26 shape together with its applicability traits, `std::invoke_r` and
`std::unreachable`. Names C++20 already has — `std::nullopt`, `std::in_place`,
`std::bad_optional_access` — are used directly and not mirrored.
SFINAE-friendly C++26 shape together with its applicability traits, `std::invoke_r` with its C++23
invocability traits, `std::unreachable`, and the C++23 traits detecting a reference bound to a
temporary. Names C++20 already has — `std::nullopt`, `std::in_place`, `std::bad_optional_access` —
are used directly and not mirrored.

The polyfills track the C++ working draft, deviating deliberately in three ways, each noted on
The polyfills track the C++ working draft, deviating deliberately in four ways, each noted on
the entity it concerns:

* where the standard leaves a member's `noexcept` specification unstated, one is derived from
Expand All @@ -23,6 +24,10 @@ the entity it concerns:
`LIBFN_ASSERT` before inclusion
* `expected`'s comparison against a value is declared at namespace scope rather than as a
hidden friend, keeping its constraint deducible
* where the compiler lacks the builtin (GCC 12, MSVC before Visual Studio 2026 18.6), the traits
detecting a reference bound to a temporary are emulated: they never report a temporary which is
not created, but miss one created by converting a class source not reference-related to the
referenced type, or one bound by a reference to an array

Members are not restated here. A `pfn` type is the standard's type, member for member, so each
one below names its standard counterpart and links to where it is specified; a second copy of
Expand Down Expand Up @@ -124,6 +129,26 @@ constexpr auto invoke_r(F &&f, Args &&...args); // (1)

:include-doxygen-doc-params: pfn::invoke_r { args: "F &&, Args &&...", title: "parameters" }

### Invocability traits {style: "api"}
The C++23 traits `invoke_r` is specified through, defined in `<pfn/type_traits.hpp>`; each also
comes in its `_v` form.

:include-doxygen-doc: pfn::is_invocable_r

:include-doxygen-doc: pfn::is_nothrow_invocable_r

---

## reference_constructs_from_temporary {style: "api"}

##### Defined in {style: "api", badge: "#include <pfn/type_traits.hpp>"}

:include-doxygen-doc: pfn::reference_constructs_from_temporary

### reference_converts_from_temporary {style: "api"}

:include-doxygen-doc: pfn::reference_converts_from_temporary

---

## unreachable {style: "api"}
Expand Down Expand Up @@ -159,6 +184,38 @@ constexpr bool is_nothrow_applicable_v = is_nothrow_applicable<Fn, Tuple>::value

:include-doxygen-doc: pfn::is_nothrow_applicable_v { args: "" }

## More invocability traits {style: "api"}

```cpp {title: "pfn::is_invocable_r_v"}
template <class R, class Fn, class... Args>
constexpr bool is_invocable_r_v = is_invocable_r<R, Fn, Args...>::value; // (1)
```

:include-doxygen-doc: pfn::is_invocable_r_v { args: "" }

```cpp {title: "pfn::is_nothrow_invocable_r_v"}
template <class R, class Fn, class... Args>
constexpr bool is_nothrow_invocable_r_v = is_nothrow_invocable_r<R, Fn, Args...>::value; // (1)
```

:include-doxygen-doc: pfn::is_nothrow_invocable_r_v { args: "" }

## More reference binding traits {style: "api"}

```cpp {title: "pfn::reference_constructs_from_temporary_v"}
template <class T, class U>
constexpr bool reference_constructs_from_temporary_v = reference_constructs_from_temporary<T, U>::value; // (1)
```

:include-doxygen-doc: pfn::reference_constructs_from_temporary_v { args: "" }

```cpp {title: "pfn::reference_converts_from_temporary_v"}
template <class T, class U>
constexpr bool reference_converts_from_temporary_v = reference_converts_from_temporary<T, U>::value; // (1)
```

:include-doxygen-doc: pfn::reference_converts_from_temporary_v { args: "" }

## Comparison against a value {style: "api"}

```cpp {title: "pfn::operator=="}
Expand Down
1 change: 1 addition & 0 deletions include/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ set(INCLUDE_PFN_HEADERS
pfn/functional.hpp
pfn/optional.hpp
pfn/tuple.hpp
pfn/type_traits.hpp
pfn/utility.hpp
)

Expand Down
14 changes: 7 additions & 7 deletions include/fn/detail/functional.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,9 @@ template <typename Fn, typename Arg, typename Arg0, typename... Args>
}

template <typename Ret, typename Fn, typename... Args>
requires(not(... || (_some_pack<Args> || _some_copack<Args>))) && ::std::is_invocable_r_v<Ret, Fn, Args...>
requires(not(... || (_some_pack<Args> || _some_copack<Args>))) && ::pfn::is_invocable_r_v<Ret, Fn, Args...>
[[nodiscard]] constexpr auto apply_r(Fn &&fn, Args &&...args) //
noexcept(::std::is_nothrow_invocable_r_v<Ret, Fn, Args...>)
noexcept(::pfn::is_nothrow_invocable_r_v<Ret, Fn, Args...>)
-> DEDUCED_RETURN(::pfn::invoke_r<Ret>(FWD(fn), FWD(args)...))
{
return ::pfn::invoke_r<Ret>(FWD(fn), FWD(args)...);
Expand All @@ -192,7 +192,7 @@ template <typename Ret, typename Fn, typename... Args>
// trailing arguments. Constrained so a non-viable call is a substitution failure before the
// noexcept-specifier can instantiate.
template <typename Ret, typename Fn, typename Tuple, ::std::size_t... Ix, typename... Args>
requires ::std::is_invocable_r_v<Ret, Fn, decltype(::std::get<Ix>(::std::declval<Tuple>()))..., Args...>
requires ::pfn::is_invocable_r_v<Ret, Fn, decltype(::std::get<Ix>(::std::declval<Tuple>()))..., Args...>
constexpr Ret _apply_r_elems(Fn &&fn, Tuple &&t, ::std::index_sequence<Ix...>, Args &&...args) //
noexcept(noexcept(::pfn::invoke_r<Ret>(FWD(fn), ::std::get<Ix>(FWD(t))..., FWD(args)...)))
{
Expand Down Expand Up @@ -241,9 +241,9 @@ template <typename Ret, typename Fn, typename Tuple, typename Ix, typename... Ar
template <typename Ret, typename Fn, typename Tuple, ::std::size_t... Ix, typename... Args>
struct _elems_probe_r<Ret, Fn, Tuple, ::std::index_sequence<Ix...>, Args...> {
static constexpr bool _invocable
= ::std::is_invocable_r_v<Ret, Fn, decltype(::std::get<Ix>(::std::declval<Tuple>()))..., Args...>;
= ::pfn::is_invocable_r_v<Ret, Fn, decltype(::std::get<Ix>(::std::declval<Tuple>()))..., Args...>;
static constexpr bool _nothrow
= ::std::is_nothrow_invocable_r_v<Ret, Fn, decltype(::std::get<Ix>(::std::declval<Tuple>()))..., Args...>;
= ::pfn::is_nothrow_invocable_r_v<Ret, Fn, decltype(::std::get<Ix>(::std::declval<Tuple>()))..., Args...>;
};

template <bool, typename Ret, typename Fn, typename Tuple, typename... Args> struct _elems_gate_r {
Expand Down Expand Up @@ -478,9 +478,9 @@ template <typename Ret, typename Tag, typename Fn, typename V, typename... Args>

template <typename Ret, typename Tag, typename Fn, typename V, typename... Args>
requires(not _some_pack<V>) && (not _some_copack<V>) && (not ::pfn::detail::_tuple_like<V>)
&& ::std::is_invocable_r_v<Ret, Fn, Tag, V, Args...>
&& ::pfn::is_invocable_r_v<Ret, Fn, Tag, V, Args...>
[[nodiscard]] constexpr auto _apply_tagged_r(Fn &&fn, V &&v, Args &&...args) //
noexcept(::std::is_nothrow_invocable_r_v<Ret, Fn, Tag, V, Args...>) -> Ret
noexcept(::pfn::is_nothrow_invocable_r_v<Ret, Fn, Tag, V, Args...>) -> Ret
{
return ::pfn::invoke_r<Ret>(FWD(fn), Tag{}, FWD(v), FWD(args)...);
}
Expand Down
Loading
Loading