expected_fs is a header-only C++23-or-newer wrapper around std::filesystem
that returns std::expected<T, std::error_code> by default instead of throwing
std::filesystem_error for ordinary filesystem failures.
- CMake 3.25+
- A C++23-or-newer compiler and standard library with
<expected>support
C++20 is not supported in v1. Configuring or compiling with a C++20-only
toolchain will fail with a clear diagnostic. By default, CMake selects C++26
when the compiler exposes cxx_std_26; otherwise it falls back to C++23. Set
expected_fs_CXX_STANDARD to 23 or 26 to pin that choice explicitly. Other
values, or a requested standard unsupported by the compiler, fail during
configuration. C++26-only filesystem facilities are enabled only when the
standard library advertises the corresponding feature-test macro.
#include <expected_fs/expected_fs.hpp>
#include <filesystem>
#include <iostream>
int main() {
const auto size = expected_fs::file_size("data.txt");
if (!size) {
std::cerr << size.error().message() << '\n';
return 1;
}
std::cout << *size << '\n';
}Most functions mirror the corresponding std::filesystem names inside the
expected_fs namespace:
auto created = expected_fs::create_directories("out/cache");
auto copied = expected_fs::copy_file("input.txt", "out/input.txt");
auto removed = expected_fs::remove("out/input.txt");The public expected alias mirrors std::expected, defaulting the error type to
std::error_code for filesystem wrappers:
template <class T, class E = std::error_code>
using expected = std::expected<T, E>;All 56 filesystem wrapper names declared directly in expected_fs are
customization point objects (CPOs). Together they cover 94 standard-compatible
call forms: 72 fallible forms and 22 non-fallible forms. A fallible call such as
expected_fs::file_size(p) uses the built-in expected_fs::std_fs filesystem
backend and expected_fs::std_result result domain by default, while explicit
backend, value, error, and result-domain calls are also supported:
auto size = expected_fs::file_size("data.txt");
auto typed = expected_fs::file_size(expected_fs::with_value<my_size>,
"data.txt");
auto mapped = expected_fs::file_size(expected_fs::with_error<my_error>,
"data.txt");
auto typed_mapped =
expected_fs::file_size(expected_fs::with_error<my_error>(
expected_fs::with_value<my_size>),
"data.txt");
auto custom_result = expected_fs::file_size(expected_fs::with_result(my_result),
"data.txt");
auto custom = expected_fs::file_size(my_fs, my_path);Fallible forms produce a result-domain value and support with_value,
with_error, and with_result. Non-fallible forms, such as
expected_fs::status_known(status) and iterator observers, return their raw
value; result adapters do not apply to them.
These names are function objects, not ordinary free functions. Existing call
expressions such as expected_fs::file_size(p) remain valid, but code that
takes a function address, stores a specific overload in a function pointer, or
otherwise depends on a free-function overload set must be updated. Moving these
APIs from free functions to customization point objects is therefore a
source-compatibility change even when ordinary calls are unchanged.
There are three separate backend customization mechanisms:
- A direct operation override provides
tag_invokefor an operation tag, such asexpected_fs::file_size_t, and the backend's arguments. This override owns the complete operation contract, including its return type. It is not automatically rewritten bywith_value,with_error, orwith_result. - A policy-aware backend provides
tag_invokeforexpected_fs::result_adaptor<Fs, Policy>. It returnsPolicy::result<T>and constructs outcomes throughPolicy::successandPolicy::failure. This is the customization to implement when the backend should participate in the falliblewith_*result adaptations. It does not apply to non-fallible forms. - A traits backend specializes
expected_fs::fs_traits<Fs>with its associated filesystem types and anopstype. Each operation implemented byopsbecomes the fallback for that CPO. Fallible primitives participate in everywith_*result adaptation; non-fallible primitives return their raw result. Backends may implement the protocol incrementally: only the primitive needed by a call is required.
Every ops primitive receives the stored backend as its first argument.
Fallible primitives additionally receive a std::error_code& as their last
argument; non-fallible primitives do not. This makes the same protocol suitable
for stateless adapters, in-memory filesystems, remote clients, and stateful test
doubles. For example, a backend that initially supports only file_size can be
defined as follows:
struct my_ops {
static std::uintmax_t file_size(my_fs& fs, const my_path& value,
std::error_code& error);
};
template <>
struct expected_fs::fs_traits<my_fs> {
using ops = my_ops;
using path = my_path;
};The complete protocol mirrors all 94 expected_fs wrapper forms, including
path queries and mutations, status predicates, directory_entry operations,
and both iterator types. Associated enum types supply the convenience-overload
defaults (none for copy/directory options and replace for permission
options). Explicit direct tag_invoke customizations always take precedence
over the traits fallback; policy-aware customizations do so for fallible forms.
The backend-first parameter is part of the protocol. An older experimental
primitive such as Ops::file_size(path, error) must be migrated to
Ops::file_size(fs, path, error).
For a custom associated type, the explicit backend is also required to reach
the traits fallback: call expected_fs::status_known(fs, status), for example,
not expected_fs::status_known(status). The library does not try to infer Fs
by reverse-matching status against fs_file_status_t<Fs>. The same rule
applies to every non-fallible associated-value form.
Backends that expose both raw status predicates and fallible path or
directory_entry predicates must use a file_status type distinct from their
path and directory_entry types. Otherwise calls such as exists(fs, value)
cannot distinguish the raw and fallible forms; an exact file_status match is
treated as the raw form.
When the first argument has an fs_traits specialization, it is always treated
as an explicit backend. If that backend does not implement the requested
primitive, the expression is ill-formed; it never falls through by converting
the backend into an argument for std_fs.
The default expected_fs::path, expected_fs::directory_iterator, and related
aliases are derived from fs_traits<expected_fs::std_fs_t>, while
expected_fs::fs_path_t<Fs> and the other fs_*_t aliases expose the types
associated with an explicit backend. Result adapters preserve those associated
types and only change the result-domain, value, and error policy.
with_value<void> discards the natural success value while preserving errors.
Result adapters created from a backend own a decayed copy of that backend by
default, including when the source backend is an lvalue. Use std::ref(fs) or
std::cref(fs) explicitly to create a borrowing adapter instead:
#include <functional>
auto owned = expected_fs::with_error<my_error>(my_fs);
auto borrowed = expected_fs::with_error<my_error>(std::ref(my_fs));
auto borrowed_const = expected_fs::with_error<my_error>(std::cref(my_fs));A borrowed backend must outlive the adapter and every operation performed through it. Borrowing does not add synchronization: the caller remains responsible for the backend's thread-safety and for coordinating shared mutable state.
The optional tl::expected adapter provides a ready-made result domain without
adding tl::expected as a dependency of the core header:
#include <expected_fs/result_domains/tl_expected.hpp>
inline constexpr auto tl_fs = expected_fs::with_result(
expected_fs::tl_expected_result, expected_fs::std_fs);
auto size = expected_fs::file_size(tl_fs, "data.txt");
auto copied = expected_fs::copy_file(tl_fs, "a.txt", "b.txt");Both results above use tl::expected. Calls without the explicit tl_fs
adaptor continue to use std::expected, so including the adapter never changes
the program-wide default.
The consumer must make tl::expected available. Its CMake package exports the
tl::expected target:
find_package(tl-expected CONFIG REQUIRED)
target_link_libraries(your_target PRIVATE
expected_fs::expected_fs
tl::expected
)Third-party expected implementations can be integrated through a minimal, stateless result-domain protocol:
- specialize
expected_fs::result_domain_traits<Domain>with aresult<T, E>alias; - provide
tag_invokeoverloads forexpected_fs::result_success_t, including thevoidsuccess form; and - provide a
tag_invokeoverload forexpected_fs::result_failure_t.
No result observer hooks are required. A domain is a type-level tag: it must be
default-constructible, and state from an object passed to with_result is not
retained. Use expected_fs::with_policy<Policy> as the low-level escape hatch
for unusual result types or behavior that does not fit this stateless protocol.
The expected_fs core is distributed as a self-contained public header. For
the simplest integration path, copy include/expected_fs/expected_fs.hpp into
your project and include it directly:
#include <expected_fs/expected_fs.hpp>The core header requires C++23 <expected> support and does not require
linking an additional library. Optional result-domain adapters are distributed
as separate headers and declare their own third-party include requirements.
Coverage is tracked against the cppreference filesystem index and the linked
path, directory_entry, directory_iterator, and
recursive_directory_iterator pages:
https://en.cppreference.com/w/cpp/filesystem.
All 56 directly named expected_fs filesystem wrappers in the table are CPOs;
they represent 94 forms (72 fallible and 22 non-fallible). Rows marked as
aliases instead expose an associated concrete type, so its native members,
operators, and ADL functions remain available. Those concrete-type APIs are not
part of the named-wrapper CPO protocol and are not counted in the 56 names.
Fallible constructors cannot return expected, so they use make_* factory
functions.
C++26-only rows are feature-gated; std::formatter<std::filesystem::path> is
available through expected_fs::path when EXPECTED_FS_HAS_FORMAT_PATH is 1
(__cpp_lib_format_path >= 202403L), and the same condition is exposed as
expected_fs::has_format_path.
| std | expected_fs | Status |
|---|---|---|
std::filesystem::path |
expected_fs::path alias |
✅ |
std::filesystem::path::format, native_format, generic_format, auto_format |
expected_fs::path::... alias |
✅ |
std::filesystem::path member types/constants |
expected_fs::path alias |
✅ |
std::filesystem::path constructors, destructor, assignment |
expected_fs::path alias |
✅ |
std::filesystem::path::assign |
expected_fs::path::assign alias |
✅ |
std::filesystem::path::append, operator/= |
expected_fs::path alias |
✅ |
std::filesystem::path::concat, operator+= |
expected_fs::path alias |
✅ |
std::filesystem::path::clear, make_preferred, remove_filename, replace_filename, replace_extension, swap |
expected_fs::path alias |
✅ |
std::filesystem::path::native, c_str, operator string_type |
expected_fs::path alias |
✅ |
std::filesystem::path::string, wstring, u8string, u16string, u32string |
expected_fs::path alias |
✅ |
std::filesystem::path::generic_string, generic_wstring, generic_u8string, generic_u16string, generic_u32string |
expected_fs::path alias |
✅ |
std::filesystem::path::compare |
expected_fs::path::compare alias |
✅ |
std::filesystem::path::lexically_normal, lexically_relative, lexically_proximate |
expected_fs::path alias |
✅ |
std::filesystem::path decomposition members |
expected_fs::path alias |
✅ |
std::filesystem::path query members |
expected_fs::path alias |
✅ |
std::filesystem::path::begin, end |
expected_fs::path alias |
✅ |
std::filesystem::path comparison operators |
expected_fs::path alias |
✅ |
std::filesystem::operator/(path, path) |
expected_fs::path alias / ADL |
✅ |
std::filesystem::operator<<, operator>> for path |
expected_fs::path alias / ADL |
✅ |
std::filesystem::swap(path&, path&) |
expected_fs::swap(path&, path&) |
✅ |
std::filesystem::hash_value(path) |
expected_fs::hash_value(path) |
✅ |
std::filesystem::u8path |
expected_fs::u8path |
✅ |
std::hash<std::filesystem::path> |
std::hash<expected_fs::path> alias |
✅ |
std::formatter<std::filesystem::path> |
std::formatter<expected_fs::path> when EXPECTED_FS_HAS_FORMAT_PATH is 1 |
✅ |
std::filesystem::filesystem_error |
expected_fs::filesystem_error alias |
✅ |
std::filesystem::directory_entry |
expected_fs::directory_entry alias |
✅ |
std::filesystem::directory_entry constructors |
expected_fs::make_directory_entry for fallible construction; alias otherwise |
✅ |
std::filesystem::directory_entry::operator= |
expected_fs::directory_entry alias |
✅ |
std::filesystem::directory_entry::assign |
expected_fs::assign(directory_entry&, path) |
✅ |
std::filesystem::directory_entry::replace_filename |
expected_fs::replace_filename(directory_entry&, path) |
✅ |
std::filesystem::directory_entry::refresh |
expected_fs::refresh(directory_entry&) |
✅ |
std::filesystem::directory_entry::path, operator const path& |
expected_fs::directory_entry alias |
✅ |
std::filesystem::directory_entry::exists |
expected_fs::exists(directory_entry) |
✅ |
std::filesystem::directory_entry::is_block_file |
expected_fs::is_block_file(directory_entry) |
✅ |
std::filesystem::directory_entry::is_character_file |
expected_fs::is_character_file(directory_entry) |
✅ |
std::filesystem::directory_entry::is_directory |
expected_fs::is_directory(directory_entry) |
✅ |
std::filesystem::directory_entry::is_fifo |
expected_fs::is_fifo(directory_entry) |
✅ |
std::filesystem::directory_entry::is_other |
expected_fs::is_other(directory_entry) |
✅ |
std::filesystem::directory_entry::is_regular_file |
expected_fs::is_regular_file(directory_entry) |
✅ |
std::filesystem::directory_entry::is_socket |
expected_fs::is_socket(directory_entry) |
✅ |
std::filesystem::directory_entry::is_symlink |
expected_fs::is_symlink(directory_entry) |
✅ |
std::filesystem::directory_entry::file_size |
expected_fs::file_size(directory_entry) |
✅ |
std::filesystem::directory_entry::hard_link_count |
expected_fs::hard_link_count(directory_entry) |
✅ |
std::filesystem::directory_entry::last_write_time |
expected_fs::last_write_time(directory_entry) |
✅ |
std::filesystem::directory_entry::status, symlink_status |
expected_fs::status(directory_entry), expected_fs::symlink_status(directory_entry) |
✅ |
std::filesystem::directory_entry comparison operators |
expected_fs::directory_entry alias |
✅ |
std::filesystem::operator<< for directory_entry |
expected_fs::directory_entry alias / ADL |
✅ |
std::filesystem::directory_iterator |
expected_fs::directory_iterator alias |
✅ |
std::filesystem::directory_iterator constructors |
expected_fs::make_directory_iterator for fallible construction; alias otherwise |
✅ |
std::filesystem::directory_iterator::operator*, operator->, operator= |
expected_fs::directory_iterator alias |
✅ |
std::filesystem::directory_iterator::increment, operator++ |
expected_fs::increment(directory_iterator&) for fallible increment; alias otherwise |
✅ |
std::filesystem::begin/end(directory_iterator) |
expected_fs::begin/end(directory_iterator) |
✅ |
std::ranges iterator helper specializations for directory_iterator |
expected_fs::directory_iterator alias |
✅ |
std::filesystem::recursive_directory_iterator |
expected_fs::recursive_directory_iterator alias |
✅ |
std::filesystem::recursive_directory_iterator constructors |
expected_fs::make_recursive_directory_iterator for fallible construction; alias otherwise |
✅ |
std::filesystem::recursive_directory_iterator::operator*, operator->, operator= |
expected_fs::recursive_directory_iterator alias |
✅ |
std::filesystem::recursive_directory_iterator::options |
expected_fs::options(recursive_directory_iterator) |
✅ |
std::filesystem::recursive_directory_iterator::depth |
expected_fs::depth(recursive_directory_iterator) |
✅ |
std::filesystem::recursive_directory_iterator::recursion_pending |
expected_fs::recursion_pending(recursive_directory_iterator) |
✅ |
std::filesystem::recursive_directory_iterator::increment, operator++ |
expected_fs::increment(recursive_directory_iterator&) for fallible increment; alias otherwise |
✅ |
std::filesystem::recursive_directory_iterator::pop |
expected_fs::pop(recursive_directory_iterator&) |
✅ |
std::filesystem::recursive_directory_iterator::disable_recursion_pending |
expected_fs::disable_recursion_pending(recursive_directory_iterator&) |
✅ |
std::filesystem::begin/end(recursive_directory_iterator) |
expected_fs::begin/end(recursive_directory_iterator) |
✅ |
std::ranges iterator helper specializations for recursive_directory_iterator |
expected_fs::recursive_directory_iterator alias |
✅ |
std::filesystem::file_status |
expected_fs::file_status alias |
✅ |
std::filesystem::space_info |
expected_fs::space_info alias |
✅ |
std::filesystem::file_type |
expected_fs::file_type alias |
✅ |
std::filesystem::file_time_type |
expected_fs::file_time_type alias |
✅ |
std::filesystem::perms |
expected_fs::perms alias |
✅ |
std::filesystem::perm_options |
expected_fs::perm_options alias |
✅ |
std::filesystem::copy_options |
expected_fs::copy_options alias |
✅ |
std::filesystem::directory_options |
expected_fs::directory_options alias |
✅ |
std::filesystem::absolute |
expected_fs::absolute |
✅ |
std::filesystem::canonical |
expected_fs::canonical |
✅ |
std::filesystem::weakly_canonical |
expected_fs::weakly_canonical |
✅ |
std::filesystem::relative |
expected_fs::relative |
✅ |
std::filesystem::proximate |
expected_fs::proximate |
✅ |
std::filesystem::copy |
expected_fs::copy |
✅ |
std::filesystem::copy_file |
expected_fs::copy_file |
✅ |
std::filesystem::copy_symlink |
expected_fs::copy_symlink |
✅ |
std::filesystem::create_directory |
expected_fs::create_directory |
✅ |
std::filesystem::create_directories |
expected_fs::create_directories |
✅ |
std::filesystem::create_hard_link |
expected_fs::create_hard_link |
✅ |
std::filesystem::create_symlink |
expected_fs::create_symlink |
✅ |
std::filesystem::create_directory_symlink |
expected_fs::create_directory_symlink |
✅ |
std::filesystem::current_path |
expected_fs::current_path |
✅ |
std::filesystem::temp_directory_path |
expected_fs::temp_directory_path |
✅ |
std::filesystem::exists |
expected_fs::exists |
✅ |
std::filesystem::equivalent |
expected_fs::equivalent |
✅ |
std::filesystem::file_size |
expected_fs::file_size |
✅ |
std::filesystem::hard_link_count |
expected_fs::hard_link_count |
✅ |
std::filesystem::last_write_time |
expected_fs::last_write_time |
✅ |
std::filesystem::permissions |
expected_fs::permissions |
✅ |
std::filesystem::read_symlink |
expected_fs::read_symlink |
✅ |
std::filesystem::remove |
expected_fs::remove |
✅ |
std::filesystem::remove_all |
expected_fs::remove_all |
✅ |
std::filesystem::rename |
expected_fs::rename |
✅ |
std::filesystem::resize_file |
expected_fs::resize_file |
✅ |
std::filesystem::space |
expected_fs::space |
✅ |
std::filesystem::status |
expected_fs::status |
✅ |
std::filesystem::symlink_status |
expected_fs::symlink_status |
✅ |
std::filesystem::is_block_file |
expected_fs::is_block_file |
✅ |
std::filesystem::is_character_file |
expected_fs::is_character_file |
✅ |
std::filesystem::is_directory |
expected_fs::is_directory |
✅ |
std::filesystem::is_empty |
expected_fs::is_empty |
✅ |
std::filesystem::status_known |
expected_fs::status_known |
✅ |
std::filesystem::is_fifo |
expected_fs::is_fifo |
✅ |
std::filesystem::is_other |
expected_fs::is_other |
✅ |
std::filesystem::is_regular_file |
expected_fs::is_regular_file |
✅ |
std::filesystem::is_socket |
expected_fs::is_socket |
✅ |
std::filesystem::is_symlink |
expected_fs::is_symlink |
✅ |
cmake -S . -B build \
-DCMAKE_INSTALL_PREFIX=/absolute/path/to/install \
-DCPM_SOURCE_CACHE=$HOME/.cache/CPM \
-Dexpected_fs_CXX_STANDARD=23
cmake --build build
ctest --test-dir build -C Debug -VV
cmake --build build --target installTo configure without tests:
cmake -S . -B build \
-Dexpected_fs_ENABLE_UNIT_TESTING=OFF \
-DCMAKE_INSTALL_PREFIX=/absolute/path/to/installAfter installation:
find_package(expected_fs CONFIG REQUIRED)
target_link_libraries(your_target PRIVATE expected_fs::expected_fs)Since this is header-only, linking the target only provides include directories, and compile features.
include(FetchContent)
FetchContent_Declare(
expected_fs
GIT_REPOSITORY https://github.com/MrZLeo/expected-fs.git
GIT_TAG v0.1.0
)
FetchContent_MakeAvailable(expected_fs)
target_link_libraries(your_target PRIVATE expected_fs::expected_fs)CPMAddPackage(
NAME expected_fs
GITHUB_REPOSITORY MrZLeo/expected-fs
GIT_TAG v0.1.0
)
target_link_libraries(your_target PRIVATE expected_fs::expected_fs)Useful local shortcuts:
make testmake coveragemake docsmake installmake format
The project uses Catch2 through CPM.cmake for tests.
This project is licensed under the BSD 3-Clause. See LICENSE for details.