On Mon, Feb 16, 2026 at 11:28 AM Jonathan Wakely <[email protected]> wrote:
> On Mon, 16 Feb 2026 at 09:25 +0100, Tomasz Kamiński wrote: > >From: Ivan Lazaric <[email protected]> > > > >This patch implements formatting for std::filesystem::path from P2845R8, > >and defines the feature test macro __cpp_lib_format_path to 202403L, > >provided only in <filesystem>. > > > >Formatting options are performed (if applicable) in order: > >'g', '?', transcoding, fill-and-align & width > > > >The standard specifies transcoding behaviour only when literal encoding > >is UTF-8, leaving all other cases implementation defined. > >Current implementation of filesystem::path assumes: > >* char encoding is UTF-8 > >* wchar_t encoding is either UTF-32 or UTF-16 > > > >libstdc++-v3/ChangeLog: > > > > * include/bits/fs_path.h: Include bits/formatfwd.h. > > formatter<filesystem::path, _CharT>): Define. > > * include/bits/version.def (format_path): Define. > > * include/bits/version.h: Regenerate. > > * include/std/filesystem: Expose __cpp_lib_format_path. > > * testsuite/std/format/fs_path.cc: New test. > > > >Reviewed-by: Tomasz Kamiński <[email protected]> > >Signed-off-by: Ivan Lazaric <[email protected]> > >Co-authored-by: Jonathan Wakely <[email protected]> > >--- > >v3: > > - updates formatting, replace spaces with tabs > > - remove __always_inline from parse method > > - extract _ViewT at begining of the format function > > - use from_range constructor to create __out_str > > - add new test for invalid UTF-8 sequence > > > >Testing on x86_64-linux. fs_patch.cc test passed with all standard > >modes, cxx11 string API, and _GLIBCXX_DEBUG. > > > >OK for trunk when all test passes? > > > OK, thank you both for getting this done. > Patch is now pushed to the trunk. > > > > libstdc++-v3/include/bits/fs_path.h | 107 +++++++++++++++ > > libstdc++-v3/include/bits/version.def | 10 ++ > > libstdc++-v3/include/bits/version.h | 10 ++ > > libstdc++-v3/include/std/filesystem | 1 + > > libstdc++-v3/testsuite/std/format/fs_path.cc | 136 +++++++++++++++++++ > > 5 files changed, 264 insertions(+) > > create mode 100644 libstdc++-v3/testsuite/std/format/fs_path.cc > > > >diff --git a/libstdc++-v3/include/bits/fs_path.h > b/libstdc++-v3/include/bits/fs_path.h > >index 07b74de6cbe..5c0d5c9d5f1 100644 > >--- a/libstdc++-v3/include/bits/fs_path.h > >+++ b/libstdc++-v3/include/bits/fs_path.h > >@@ -50,6 +50,10 @@ > > # include <compare> > > #endif > > > >+#ifdef __glibcxx_format_path // C++ >= 26 && HOSTED > >+# include <bits/formatfwd.h> > >+#endif > >+ > > #if defined(_WIN32) && !defined(__CYGWIN__) > > # define _GLIBCXX_FILESYSTEM_IS_WINDOWS 1 > > #endif > >@@ -1451,6 +1455,109 @@ template<> > > { return filesystem::hash_value(__p); } > > }; > > > >+#ifdef __glibcxx_format_path // C++ >= 26 && HOSTED > >+ template<__format::__char _CharT> > >+ struct formatter<filesystem::path, _CharT> > >+ { > >+ formatter() = default; > >+ > >+ constexpr typename basic_format_parse_context<_CharT>::iterator > >+ parse(basic_format_parse_context<_CharT>& __pc) > >+ { > >+ auto __first = __pc.begin(); > >+ const auto __last = __pc.end(); > >+ __format::_Spec<_CharT> __spec{}; > >+ > >+ auto __finalize = [this, &__spec] { > >+ _M_spec = __spec; > >+ }; > >+ > >+ auto __finished = [&] { > >+ if (__first == __last || *__first == '}') > >+ { > >+ __finalize(); > >+ return true; > >+ } > >+ return false; > >+ }; > >+ > >+ if (__finished()) > >+ return __first; > >+ > >+ __first = __spec._M_parse_fill_and_align(__first, __last); > >+ if (__finished()) > >+ return __first; > >+ > >+ __first = __spec._M_parse_width(__first, __last, __pc); > >+ if (__finished()) > >+ return __first; > >+ > >+ if (*__first == '?') > >+ { > >+ __spec._M_debug = true; > >+ ++__first; > >+ } > >+ if (__finished()) > >+ return __first; > >+ > >+ if (*__first == 'g') > >+ { > >+ __spec._M_type = __format::_Pres_g; > >+ ++__first; > >+ } > >+ if (__finished()) > >+ return __first; > >+ > >+ __format::__failed_to_parse_format_spec(); > >+ } > >+ > >+ template<typename _Out> > >+ typename basic_format_context<_Out, _CharT>::iterator > >+ format(const filesystem::path& __p, > >+ basic_format_context<_Out, _CharT>& __fc) const > >+ { > >+ using _ValueT = filesystem::path::value_type; > >+ using _ViewT = basic_string_view<_ValueT>; > >+ using _FmtStrT = __format::__formatter_str<_CharT>; > >+ > >+ _ViewT __sv; > >+ filesystem::path::string_type __s; > >+ if (_M_spec._M_type == __format::_Pres_g) > >+ __sv = __s = __p.generic_string<_ValueT>(); > >+ else > >+ __sv = __p.native(); > >+ > >+ auto __spec = _M_spec; > >+ // 'g' should not be passed along. > >+ __spec._M_type = __format::_Pres_none; > >+ > >+ if constexpr (is_same_v<_CharT, _ValueT>) > >+ return _FmtStrT(__spec).format(__sv, __fc); > >+ else > >+ { > >+ __format::_Str_sink<_ValueT> __sink; > >+ if (__spec._M_debug) > >+ { > >+ using __format::_Term_quote; > >+ __format::__write_escaped(__sink.out(), __sv, > _Term_quote); > >+ __sv = __sink.view(); > >+ __spec._M_debug = 0; > >+ } > >+ basic_string<_CharT> __out_str > >+ (std::from_range, __unicode::_Utf_view<_CharT, > _ViewT>(__sv)); > >+ return _FmtStrT(__spec).format(__out_str, __fc); > >+ } > >+ } > >+ > >+ constexpr void > >+ set_debug_format() noexcept > >+ { _M_spec._M_debug = true; } > >+ > >+ private: > >+ __format::_Spec<_CharT> _M_spec{}; > >+ }; > >+#endif // __glibcxx_format_path > >+ > > _GLIBCXX_END_NAMESPACE_VERSION > > } // namespace std > > > >diff --git a/libstdc++-v3/include/bits/version.def > b/libstdc++-v3/include/bits/version.def > >index 4b8e9d43ec2..c7709ba3a07 100644 > >--- a/libstdc++-v3/include/bits/version.def > >+++ b/libstdc++-v3/include/bits/version.def > >@@ -1561,6 +1561,16 @@ ftms = { > > }; > > }; > > > >+ftms = { > >+ name = format_path; > >+ // 202403 P2845R8 Formatting of std::filesystem::path > >+ values = { > >+ v = 202403; > >+ cxxmin = 26; > >+ hosted = yes; > >+ }; > >+}; > >+ > > ftms = { > > name = freestanding_algorithm; > > values = { > >diff --git a/libstdc++-v3/include/bits/version.h > b/libstdc++-v3/include/bits/version.h > >index 7602225cb6d..c72cda506f1 100644 > >--- a/libstdc++-v3/include/bits/version.h > >+++ b/libstdc++-v3/include/bits/version.h > >@@ -1721,6 +1721,16 @@ > > #endif /* !defined(__cpp_lib_format_ranges) */ > > #undef __glibcxx_want_format_ranges > > > >+#if !defined(__cpp_lib_format_path) > >+# if (__cplusplus > 202302L) && _GLIBCXX_HOSTED > >+# define __glibcxx_format_path 202403L > >+# if defined(__glibcxx_want_all) || defined(__glibcxx_want_format_path) > >+# define __cpp_lib_format_path 202403L > >+# endif > >+# endif > >+#endif /* !defined(__cpp_lib_format_path) */ > >+#undef __glibcxx_want_format_path > >+ > > #if !defined(__cpp_lib_freestanding_algorithm) > > # if (__cplusplus >= 202100L) > > # define __glibcxx_freestanding_algorithm 202311L > >diff --git a/libstdc++-v3/include/std/filesystem > b/libstdc++-v3/include/std/filesystem > >index f902c6feb77..b9900f49c33 100644 > >--- a/libstdc++-v3/include/std/filesystem > >+++ b/libstdc++-v3/include/std/filesystem > >@@ -37,6 +37,7 @@ > > #include <bits/requires_hosted.h> > > > > #define __glibcxx_want_filesystem > >+#define __glibcxx_want_format_path > > #include <bits/version.h> > > > > #ifdef __cpp_lib_filesystem // C++ >= 17 && HOSTED > >diff --git a/libstdc++-v3/testsuite/std/format/fs_path.cc > b/libstdc++-v3/testsuite/std/format/fs_path.cc > >new file mode 100644 > >index 00000000000..b91ae6fd449 > >--- /dev/null > >+++ b/libstdc++-v3/testsuite/std/format/fs_path.cc > >@@ -0,0 +1,136 @@ > >+// { dg-do run { target c++26 } } > >+// { dg-options "-fexec-charset=UTF-8" } > >+ > >+#include <filesystem> > >+#include <format> > >+#include <testsuite_hooks.h> > >+ > >+using std::filesystem::path; > >+ > >+template<typename... Args> > >+bool > >+is_format_string_for(const char* str, Args&&... args) > >+{ > >+ try { > >+ (void) std::vformat(str, std::make_format_args(args...)); > >+ return true; > >+ } catch (const std::format_error&) { > >+ return false; > >+ } > >+} > >+ > >+template<typename... Args> > >+bool > >+is_format_string_for(const wchar_t* str, Args&&... args) > >+{ > >+ try { > >+ (void) std::vformat(str, std::make_wformat_args(args...)); > >+ return true; > >+ } catch (const std::format_error&) { > >+ return false; > >+ } > >+} > >+ > >+void > >+test_format_spec() > >+{ > >+ // [fs.path.fmtr.funcs] > >+ // \nontermdef{path-format-spec}\br > >+ // \opt{fill-and-align} \opt{width} \opt{\terminal{?}} > \opt{\terminal{g}} > >+ path p; > >+ VERIFY( is_format_string_for("{}", p) ); > >+ VERIFY( is_format_string_for("{:}", p) ); > >+ VERIFY( is_format_string_for("{:?}", p) ); > >+ VERIFY( is_format_string_for("{:g}", p) ); > >+ VERIFY( is_format_string_for("{:?g}", p) ); > >+ VERIFY( is_format_string_for("{:?g}", p) ); > >+ VERIFY( is_format_string_for("{:F^32?g}", p) ); > >+ VERIFY( is_format_string_for("{:G<{}?g}", p, 32) ); > >+ VERIFY( is_format_string_for(L"{:G<{}?g}", p, 32) ); > >+ > >+ VERIFY( ! is_format_string_for("{:g?}", p) ); > >+} > >+ > >+#define WIDEN_(C, S) ::std::__format::_Widen<C>(S, L##S) > >+#define WFMT(S) WIDEN_(_FCharT, S) > >+#define WPATH(S) WIDEN_(_PCharT, S) > >+ > >+template<typename _FCharT, typename _PCharT> > >+void > >+test_format() > >+{ > >+ std::basic_string<_FCharT> res; > >+ res = std::format(WFMT("{}"), path(WPATH("/usr/include"))); > >+ VERIFY( res == WFMT("/usr/include") ); > >+ res = std::format(WFMT("{:.<10}"), path(WPATH("foo/bar"))); > >+ VERIFY( res == WFMT("foo/bar...") ); > >+ res = std::format(WFMT("{}"), path(WPATH("foo///bar"))); > >+ VERIFY( res == WFMT("foo///bar") ); > >+ res = std::format(WFMT("{:g}"), path(WPATH("foo///bar"))); > >+ VERIFY( res == WFMT("foo/bar") ); > >+ res = std::format(WFMT("{}"), path(WPATH("/path/with/new\nline"))); > >+ VERIFY( res == WFMT("/path/with/new\nline") ); > >+ res = std::format(WFMT("{:?}"), path(WPATH("multi\nline"))); > >+ VERIFY( res == WFMT("\"multi\\nline\"") ); > >+ res = std::format(WFMT("{:?g}"), path(WPATH("mu///lti\nli///ne"))); > >+ VERIFY( res == WFMT("\"mu/lti\\nli/ne\"") ); > >+ res = std::format(WFMT("{}"), > >+ > > path(WPATH("\u0428\u0447\u0443\u0447\u044B\u043D\u0448\u0447\u044B\u043D\u0430"))); > >+ VERIFY( res == > WFMT("\u0428\u0447\u0443\u0447\u044B\u043D\u0448\u0447\u044B\u043D\u0430")); > >+ > >+ if constexpr (path::preferred_separator == L'\\') > >+ { > >+ res = std::format(WFMT("{}"), path(WPATH("C:\\foo\\bar"))); > >+ VERIFY( res == WFMT("C:\\foo\\bar") ); > >+ res = std::format(WFMT("{:g}"), path(WPATH("C:\\foo\\bar"))); > >+ VERIFY( res == WFMT("C:/foo/bar") ); > >+ } > >+} > >+ > >+void > >+test_format_invalid() > >+{ > >+ if constexpr (std::is_same_v<path::value_type, char>) > >+ { > >+ std::wstring res; > >+ std::string_view seq = "\xf0\x9f\xa6\x84"; // \U0001F984 > >+ > >+ path p(seq.substr(1)); > >+ res = std::format(L"{}", p); > >+ VERIFY( res == L"\uFFFD\uFFFD\uFFFD" ); > >+ res = std::format(L"{:?}", p); > >+ VERIFY( res == LR"("\x{9f}\x{a6}\x{84}")" ); > >+ } > >+ else > >+ { > >+ std::string res; > >+ > >+ path p(L"\xd800"); > >+ res = std::format("{}", p); > >+ VERIFY( res == "\uFFFD" ); > >+ res = std::format("{:?}", p); > >+ VERIFY( res == "\"\\x{d800}\"" ); > >+ > >+ path p2(L"///\xd800"); > >+ res = std::format("{}", p2); > >+ VERIFY( res == "///\uFFFD" ); > >+ res = std::format("{:g}", p2); > >+ VERIFY( res == "/\uFFFD" ); > >+ res = std::format("{:?}", p2); > >+ VERIFY( res == "\"///\\x{d800}\"" ); > >+ res = std::format("{:?g}", p2); > >+ VERIFY( res == "\"/\\x{d800}\"" ); > >+ res = std::format("{:C>14?g}", p2); > >+ VERIFY( res == "CCC\"/\\x{d800}\"" ); > >+ } > >+} > >+ > >+int main() > >+{ > >+ test_format_spec(); > >+ test_format<char, char>(); > >+ test_format<char, wchar_t>(); > >+ test_format<wchar_t, char>(); > >+ test_format<wchar_t, wchar_t>(); > >+ test_format_invalid(); > >+} > >-- > >2.53.0 > > > > > >
