[PATCH v3] libstdc++: implement formatter for std::filesystem::path

Jonathan Wakely jwakely@redhat.com
Mon Feb 16 10:27:58 GMT 2026


On Mon, 16 Feb 2026 at 09:25 +0100, Tomasz Kamiński wrote:
>From: Ivan Lazaric <ivan.lazaric1@gmail.com>
>
>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 <tkaminsk@redhat.com>
>Signed-off-by: Ivan Lazaric <ivan.lazaric1@gmail.com>
>Co-authored-by: Jonathan Wakely <jwakely@redhat.com>
>---
>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.


> 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
>
>



More information about the Libstdc++ mailing list