When an and_then callback returns a carrier by lvalue reference, and_then copies it into its return value. Its constraints and noexcept specification omit that copy. If the copy throws, an incorrectly declared noexcept call terminates. If the carrier cannot be copied, just::and_then passes its constraints and then fails to compile inside the function body; optional::and_then cannot even be probed, because its deduced return type instantiates the body.
This affects fn::just<T>, fn::just<void>, fn::just<T&>, fn::optional<T>, and fn::optional<T&>. Additionally, pfn::optional<T> and pfn::optional<T&> are also affected, where noexcept is an extension.
The following reproducer compiles cleanly on g++ 16.2.1 and clang++ 22.1.8 with -std=c++20 -Wall -Wextra -Wpedantic -Werror. The assertions demonstrate the incorrect behaviour:
#include <fn/and_then.hpp>
#include <fn/just.hpp>
#include <fn/optional.hpp>
#include <memory>
// A carrier whose copy may throw, returned from a noexcept callback by lvalue reference
struct ThrowingCopy {
ThrowingCopy() = default;
ThrowingCopy(ThrowingCopy const &) noexcept(false) {}
};
inline fn::just<ThrowingCopy> gj{};
inline fn::optional<ThrowingCopy> go{};
inline fn::just<std::unique_ptr<int>> gu{};
inline int gi = 1;
constexpr auto retJ = [](auto &&...) noexcept -> fn::just<ThrowingCopy> & { return gj; };
constexpr auto retO = [](auto &&...) noexcept -> fn::optional<ThrowingCopy> & { return go; };
constexpr auto retU = [](auto &&...) noexcept -> fn::just<std::unique_ptr<int>> & { return gu; };
// Each member returns the callback's carrier by value, copying it, yet promises noexcept
static_assert(noexcept(fn::just<int>{1}.and_then(retJ)));
static_assert(noexcept(fn::just<void>{}.and_then(retJ)));
static_assert(noexcept(fn::optional<int>{1}.and_then(retO)));
static_assert(noexcept(fn::optional<int &>{gi}.and_then(retO)));
// The copy is not part of the constraint either: the member is viable for a non-copyable result,
// and calling it is a hard error
template <typename S, typename F>
concept member_and_then = requires(S s, F f) { s.and_then(f); };
static_assert(member_and_then<fn::just<int>, decltype(retU)>);
int main() {}
The first four assertions show that and_then promises noexcept even though copying the returned carrier may throw. The final assertion shows that its constraints accept a non-copyable carrier returned by reference. Adding fn::just<int>{1}.and_then(retU); then fails inside just::and_then with a deleted-copy-constructor error on both compilers.
The fix should account for this copy in both the constraints and noexcept, including through the pipeline and_then. Prvalue results should remain unaffected: guaranteed copy elision means they need no copy or move.
When an
and_thencallback returns a carrier by lvalue reference,and_thencopies it into its return value. Its constraints andnoexceptspecification omit that copy. If the copy throws, an incorrectly declarednoexceptcall terminates. If the carrier cannot be copied,just::and_thenpasses its constraints and then fails to compile inside the function body;optional::and_thencannot even be probed, because its deduced return type instantiates the body.This affects
fn::just<T>,fn::just<void>,fn::just<T&>,fn::optional<T>, andfn::optional<T&>. Additionally,pfn::optional<T>andpfn::optional<T&>are also affected, wherenoexceptis an extension.The following reproducer compiles cleanly on g++ 16.2.1 and clang++ 22.1.8 with
-std=c++20 -Wall -Wextra -Wpedantic -Werror. The assertions demonstrate the incorrect behaviour:The first four assertions show that
and_thenpromisesnoexcepteven though copying the returned carrier may throw. The final assertion shows that its constraints accept a non-copyable carrier returned by reference. Addingfn::just<int>{1}.and_then(retU);then fails insidejust::and_thenwith a deleted-copy-constructor error on both compilers.The fix should account for this copy in both the constraints and
noexcept, including through the pipelineand_then. Prvalue results should remain unaffected: guaranteed copy elision means they need no copy or move.