Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7d8e1c0
Refactor pfn::expected to move storage to a separate class
Bronek May 13, 2026
0205f89
Fixes and further refactoring
Bronek May 14, 2026
11d197e
MSVC workaround
Bronek May 14, 2026
e045baf
Add LCOV_EXCL for construct_at of a trivial type
Bronek May 14, 2026
fec147b
Another MSVC workaround
Bronek May 14, 2026
25cbace
Address review comment, fix coverage excludes
Bronek May 14, 2026
c400427
Fix exception safety of copy/move constructors
Bronek May 14, 2026
73b8589
Final cleanup
Bronek May 15, 2026
cc7b3a2
Experimental removal of include <expected>
Bronek May 15, 2026
581a5a9
Merge branch 'main' into bronek/experimental_fn_base_refactor
Bronek May 22, 2026
c46d1f9
Formatting fixes
Bronek May 22, 2026
f8b2bba
Merge branch 'main' into bronek/experimental_fn_base_refactor
Bronek May 23, 2026
30aa651
Merge branch 'main' into bronek/experimental_fn_base_refactor
Bronek May 23, 2026
1f3c02e
Draft refactor of fn::expected
Bronek May 23, 2026
6f8afad
Merge branch 'main' into bronek/experimental_fn_base_refactor
Bronek May 30, 2026
957715e
Merge branch 'main' into bronek/experimental_fn_base_refactor
Bronek Jun 3, 2026
0db15e5
Merge branch 'main' into bronek/experimental_fn_base_refactor
Bronek Jun 3, 2026
c28289b
Fix Bazel builds
Bronek Jun 3, 2026
0eb827f
Merge branch 'main' into bronek/experimental_fn_base_refactor
Bronek Jun 3, 2026
1465b2b
Work around clang-18 or older miscompile in _or_else void+value path
Bronek Jun 3, 2026
4160fea
Minor cleanup in .clangd and CLAUDE.md
Bronek Jun 4, 2026
b898ff0
Merge branch 'main' into bronek/experimental_fn_base_refactor
Bronek Jun 5, 2026
4fe6205
Add fn::expected polyfill test nesting the pfn suite
Bronek Jun 5, 2026
88c53a7
Drop fn::expected polyfill tests now covered by nested pfn suite
Bronek Jun 5, 2026
4788cee
Cover move-only and immovable value/error types in expected tests
Bronek Jun 5, 2026
9c20049
Pin expected forwarding-ctor same-type exclusion against rot
Bronek Jun 5, 2026
e33710f
Make trivial expected move ctors noexcept; NOSONAR move/swap (S5018)
Bronek Jun 5, 2026
bc9884a
NOSONAR S6031 on expected's unexpected ctors (forward<GF> is spec wor…
Bronek Jun 5, 2026
c5ebd00
Simplify clang-18 _or_else workaround to return std::move(type{...})
Bronek Jun 5, 2026
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
2 changes: 1 addition & 1 deletion .clangd
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
CompileFlags:
Add: [-Wall, -Wextra, -Wpedantic, -Wno-missing-braces, -Wno-c2y-extensions]
Remove: -W*
Remove: [-W*, -funreachable-traps]
Compiler: clang++
Diagnostics:
ClangTidy:
Expand Down
3 changes: 1 addition & 2 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,5 @@ cc_library(
name = "fn",
hdrs = glob(["include/fn/**/*.hpp"]),
includes = ["include"],
# TODO uncomment once fn is reimplemented in terms of pfn.
# deps = [":pfn"],
deps = [":pfn"],
)
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Conventions for AI agents in this repo (you are the primary reader — keep this

## Commits

- Trailer `Assisted-by: Claude:<exact session model id>` (Linux-kernel convention), e.g. `claude-opus-4-7`. No `Co-Authored-By:`.
- Trailer `Assisted-by: Claude:<exact session model id>` (Linux-kernel convention), e.g. `claude-opus-4-8`. No `Co-Authored-By:`.
- Offer commits; never commit without confirmation. Terse messages: imperative topic, body only if needed.
- Never `git push` or sign commits — the user signs (GPG) and pushes.

Expand Down
14 changes: 7 additions & 7 deletions examples/polygon/polygon.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,19 @@ struct parameters {
std::string_view const program_name = (args.size() >= 1 && !args[0].empty()) //
? args[0]
: std::string_view{"<program>"};
return std::unexpected(too_few_parameters{program_name});
return ::pfn::unexpected(too_few_parameters{program_name});
}

parameters params;
params.characters = args[1];
if (params.characters.size() < 3) {
return std::unexpected(too_few_characters{});
return ::pfn::unexpected(too_few_characters{});
}
params.required = static_cast<unsigned char>(params.characters[0]);

for (char c : params.characters) {
if (static_cast<unsigned char>(c) > 0x7F) {
return std::unexpected(non_ascii_characters{});
return ::pfn::unexpected(non_ascii_characters{});
}
}

Expand Down Expand Up @@ -185,12 +185,12 @@ struct inputs {
ec == std::errc::no_such_file_or_directory //
|| ec == std::errc::not_a_directory //
|| type == std::filesystem::file_type::not_found) {
return std::unexpected(file_not_found{{.path = std::move(path), .ec = ec}});
return ::pfn::unexpected(file_not_found{{.path = std::move(path), .ec = ec}});
}
if (ec == std::errc::permission_denied) {
return std::unexpected(permission_denied{{.path = std::move(path), .ec = ec}});
return ::pfn::unexpected(permission_denied{{.path = std::move(path), .ec = ec}});
}
return std::unexpected(
return ::pfn::unexpected(
io_error{{.path = std::move(path), .ec = (ec ? ec : std::make_error_code(std::io_errc::stream))}});
};

Expand Down Expand Up @@ -254,7 +254,7 @@ constexpr inline struct algorithm_t {
if (source.in->eof() && !source.in->bad()) {
continue;
}
return std::unexpected(read_error{{.path = source.path, .ec = std::make_error_code(std::io_errc::stream)}});
return ::pfn::unexpected(read_error{{.path = source.path, .ec = std::make_error_code(std::io_errc::stream)}});
}

return 0;
Expand Down
28 changes: 14 additions & 14 deletions examples/simple/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ TEST_CASE("Minimal expected", "[expected][and_then]")
}
{
// example-expected-and_then-error
fn::expected<double, Error> ex = std::unexpected<Error>{"Not good"};
fn::expected<double, Error> ex = ::pfn::unexpected<Error>{"Not good"};

auto oops = ex
| fn::and_then([](auto&& v) -> fn::expected<unsigned, Error> {
Expand Down Expand Up @@ -139,14 +139,14 @@ TEST_CASE("Demo expected", "[expected][pack][and_then][discard][transform_error]
if (std::from_chars(str.begin(), end, tmp).ptr == end) {
return {tmp};
}
return std::unexpected<Error>{"Failed to parse " + std::string(str)};
return ::pfn::unexpected<Error>{"Failed to parse " + std::string(str)};
};

// Immovable operations must be captured as lvalues, and functor will store
// reference to them rather than make a copy
constexpr auto fn1 = [j = ImmovableValue{-1}](int i) noexcept -> fn::expected<double, Error> {
if (i < j.value) {
return std::unexpected<Error>{"Too small"};
return ::pfn::unexpected<Error>{"Too small"};
}
return {i + 0.5};
};
Expand Down Expand Up @@ -199,12 +199,12 @@ TEST_CASE("Demo expected", "[expected][pack][and_then][discard][transform_error]
if (std::from_chars(str.begin(), end, tmp).ptr == end) {
return {tmp};
}
return std::unexpected<Error>{"Failed to parse " + std::string(str)};
return ::pfn::unexpected<Error>{"Failed to parse " + std::string(str)};
};

constexpr auto parse_twelve = [](std::string str) noexcept -> fn::expected<double, Error> {
if (str != "12")
return std::unexpected<Error>{"Not 12"};
return ::pfn::unexpected<Error>{"Not 12"};
return {12.};
};

Expand Down Expand Up @@ -235,7 +235,7 @@ TEST_CASE("Demo expected", "[expected][pack][and_then][discard][transform_error]
| fn::inspect_error([](Error) noexcept { CHECK(false); }) //
| fn::discard();

fn::expected<int, Error>{std::unexpected<Error>{"discarded"}} //
fn::expected<int, Error>{::pfn::unexpected<Error>{"discarded"}} //
| fn::inspect([](int) noexcept { CHECK(false); }) //
| fn::inspect_error([](Error e) noexcept { REQUIRE(e.what == "discarded"); }) //
| fn::discard();
Expand Down Expand Up @@ -506,7 +506,7 @@ TEST_CASE("Demo choice and graded monad", "[choice][and_then][inspect][transform
if constexpr (std::is_same_v<std::decay_t<decltype(v)>, T>) {
return {FWD(v)};
} else
return std::unexpected<InputError>{InvalidType};
return ::pfn::unexpected<InputError>{InvalidType};
});
};

Expand All @@ -523,7 +523,7 @@ TEST_CASE("Demo choice and graded monad", "[choice][and_then][inspect][transform
else if (configuration == "test")
return type{std::in_place, fn::sum{fn::pack{std::type_identity<ConfigTest>{}, std::move(test_name)}}};
else
return type{std::unexpect, InvalidConfiguration};
return type{::pfn::unexpect, InvalidConfiguration};
})(configuration, test_name) //
& convert(std::in_place_type<std::string_view>, hostname) //
& convert(std::in_place_type<long>, port) //
Expand All @@ -550,24 +550,24 @@ TEST_CASE("Demo choice and graded monad", "[choice][and_then][inspect][transform
|| config.hostname.find_first_not_of("abcdefghijklmnopqrstuvwxyz0123456789.")
!= std::string_view::npos
|| config.hostname.find("..") != std::string_view::npos)
return std::unexpected<ConfigError>(InvalidHostname);
return ::pfn::unexpected<ConfigError>(InvalidHostname);
else if (config.port < 1 || config.port > 0xffff)
return std::unexpected<ConfigError>(InvalidPort);
return ::pfn::unexpected<ConfigError>(InvalidPort);
else if (config.filename.size() < 1 || config.filename.size() > 254)
return std::unexpected<ConfigError>(InvalidFilename);
return ::pfn::unexpected<ConfigError>(InvalidFilename);
else if (config.threshold < 0 || config.threshold > 1)
return std::unexpected<ConfigError>(InvalidThreshold);
return ::pfn::unexpected<ConfigError>(InvalidThreshold);

if constexpr (std::is_same_v<std::remove_cvref_t<decltype(config)>, ConfigTest>) {
if (config.test_name != "foo")
return std::unexpected<ConfigError>(InvalidTest);
return ::pfn::unexpected<ConfigError>(InvalidTest);
}

return FWD(config);
})
| fn::and_then([](auto const &config) -> fn::expected<int, NetworkError> {
if (config.port < 1024)
return std::unexpected<NetworkError>(ConnectError);
return ::pfn::unexpected<NetworkError>(ConnectError);
if constexpr (std::is_same_v<decltype(config), ConfigProd const &>)
return {0x50eda7a}; // dummy result
else
Expand Down
3 changes: 1 addition & 2 deletions include/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@ if(NOT DISABLE_CXX23)
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
set_target_properties(include_fn PROPERTIES EXPORT_NAME fn)
# TODO uncomment once fn is reimplemented in terms of pfn.
# target_link_libraries(include_fn INTERFACE include_pfn)
target_link_libraries(include_fn INTERFACE include_pfn)
target_compile_features(include_fn INTERFACE cxx_std_23)

install(TARGETS include_fn
Expand Down
2 changes: 1 addition & 1 deletion include/fn/concepts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ concept same_monadic_type_as = same_kind<T, U> && same_value_kind<T, U>;
*/
template <class T>
concept convertible_to_unexpected
= requires { static_cast<std::unexpected<std::remove_cvref_t<T>>>(std::declval<T>()); };
= requires { static_cast<::pfn::unexpected<std::remove_cvref_t<T>>>(std::declval<T>()); };

/**
* @brief TODO
Expand Down
Loading
Loading