On Tue, 23 Sep 2025 at 14:31 +0200, Tomasz KamiƄski wrote:
The r16-3996-gdc78d691c5e5f7 commit (resolution of LWG4257) constrained the
operator<< for local_time, but didn't update the corresponding formatter. This
meant it didn't conditionally support formatting with an empty format spec 
("{}"),
which is defined in terms of operator<<.

This patch addresses that by initializing __defSpec for the local_time
formatter in the same manner as it's done for sys_time. This functionality is
extracted to the _S_spec_for_tp function of __formatter_duration. As formatting
of local_time is defined and constrained in terms of operator<< for sys_time,
we can check the viability of the ostream operator for sys_time in both cases.

As default _M_chrono_spec may now be empty for local_time, the parse method
now checks if it was supplied in the format string, similarly to sys_time. The
condition for performing runtime check is expressed directly by checking if a
non-empty default is provided. This avoids the need to access the value of
__stream_insertable outside of the __defSpec computation.

As a note, despite their similar behavior, formatters sys_time and local_time
cannot be easily defined in terms of each other, as sys_time provides time zone
information while local_time does not.

libstdc++-v3/ChangeLog:

        * include/bits/chrono_io.h (__formatter_duration::_S_spec_for_tp):
        Extracted from defition of formatter<sys_time>::__defSpec.
        (formatter<chrono::sys_time<_Duration>, _CharT>::parse): Simplify
        condition in if contexpr.
        (formatter<chrono::sys_time<_Duration>, _CharT>::__stream_insertable):
        Remove.
        (formatter<chrono::sys_time<_Duration>, _CharT>::__defSpec)
        (formatter<chrono::local_time<_Duration>, _CharT>::__defSpec):
        Compute using __formatter_duration::_S_spec_for_tp.
        (forrmatter<chrono::sys_time<_Duration>, _CharT>::parse): Check if
        parse _M_chrono_spec
        * testsuite/std/time/format/empty_spec.cc: Extend tests for floating
        point and other non-streamable durations (years).
---
Testing on x86_64-linux. OK for trunk?

Thanks, OK with one whitespace fix (see below).


libstdc++-v3/include/bits/chrono_io.h         | 59 ++++++++++---------
.../testsuite/std/time/format/empty_spec.cc   | 35 +++++++----
2 files changed, 54 insertions(+), 40 deletions(-)

diff --git a/libstdc++-v3/include/bits/chrono_io.h 
b/libstdc++-v3/include/bits/chrono_io.h
index 0a6a3a5ce5a..b1b83da8386 100644
--- a/libstdc++-v3/include/bits/chrono_io.h
+++ b/libstdc++-v3/include/bits/chrono_io.h
@@ -1929,6 +1929,24 @@ namespace __format
          return __res;
        };

+      template<typename _Duration>
+       static consteval
+       _ChronoSpec<_CharT>
+       _S_spec_for_tp()
+       {
+         using enum _ChronoParts;
+         // streaming local time is defined in terms of sys_time
+         constexpr bool __stream_insertable =
+           requires (basic_ostream<_CharT>& __os, chrono::sys_time<_Duration> 
__t)
+             { __os << __t; };
+         if constexpr (!__stream_insertable)
+           return _S_spec_for<_Duration>(_None);
+         else if constexpr (is_convertible_v<_Duration, chrono::days>)
+           return _S_spec_for<_Duration>(_Date);
+         else
+           return _S_spec_for<_Duration>(_DateTime);
+       }
+
      using __formatter_chrono<_CharT>::__formatter_chrono;
      using __formatter_chrono<_CharT>::_M_spec;

@@ -2912,12 +2930,12 @@ namespace __format
      parse(basic_format_parse_context<_CharT>& __pc)
      {
        using enum __format::_ChronoParts;
-       auto __next
+       auto __res
          = _M_f.template _M_parse<_Duration>(__pc, _ZonedDateTime, __defSpec);
-       if constexpr (!__stream_insertable)
+       if constexpr (__defSpec._M_chrono_specs.empty())
          if (_M_f._M_spec._M_chrono_specs.empty())
            __format::__invalid_chrono_spec(); // chrono-specs can't be empty
-       return __next;
+       return __res;
     }

     template<typename _Out>
@@ -2935,21 +2953,8 @@ namespace __format
       }

    private:
-      static constexpr bool __stream_insertable
-       = requires (basic_ostream<_CharT>& __os,
-                   chrono::sys_time<_Duration> __t) { __os << __t; };
-
-      static constexpr __format::_ChronoSpec<_CharT> __defSpec = []
-       {
-         using enum __format::_ChronoParts;
-         __format::_ChronoParts __needed = _DateTime;
-         if constexpr (!__stream_insertable)
-           __needed = _None;
-         else if constexpr (is_convertible_v<_Duration, chrono::days>)
-           __needed = _Date;
-         return __format::__formatter_duration<_CharT>::
-                  template _S_spec_for<_Duration>(__needed);
-       }();
+      static constexpr __format::_ChronoSpec<_CharT> __defSpec =
+       __format::__formatter_duration<_CharT>::template 
_S_spec_for_tp<_Duration>();

      __format::__formatter_duration<_CharT> _M_f{__defSpec};
    };
@@ -3110,7 +3115,12 @@ namespace __format
      parse(basic_format_parse_context<_CharT>& __pc)
      {
        using enum __format::_ChronoParts;
-       return _M_f.template _M_parse<_Duration>(__pc, _DateTime, __defSpec);
+       auto __res
+         = _M_f.template _M_parse<_Duration>(__pc, _DateTime, __defSpec);
+       if constexpr (__defSpec._M_chrono_specs.empty())
+         if (_M_f._M_spec._M_chrono_specs.empty())

There are two leading spaces before the Tab here.

I use this in my git config to highlight whitespace errors in diffs:

[diff]
        wsErrorHighlight = old,new

+           __format::__invalid_chrono_spec(); // chrono-specs can't be empty
+       return __res;
      }

      template<typename _Out>
@@ -3126,15 +3136,8 @@ namespace __format
        }

    private:
-      static constexpr __format::_ChronoSpec<_CharT> __defSpec = []
-       {
-         using enum __format::_ChronoParts;
-         __format::_ChronoParts __needed = _DateTime;
-         if constexpr (is_convertible_v<_Duration, chrono::days>)
-           __needed = _Date;
-         return __format::__formatter_duration<_CharT>::
-                  template _S_spec_for<_Duration>(__needed);
-       }();
+      static constexpr __format::_ChronoSpec<_CharT> __defSpec =
+       __format::__formatter_duration<_CharT>::template 
_S_spec_for_tp<_Duration>();

      __format::__formatter_duration<_CharT> _M_f{__defSpec};
    };
diff --git a/libstdc++-v3/testsuite/std/time/format/empty_spec.cc 
b/libstdc++-v3/testsuite/std/time/format/empty_spec.cc
index ef1b19d688c..a20c074018e 100644
--- a/libstdc++-v3/testsuite/std/time/format/empty_spec.cc
+++ b/libstdc++-v3/testsuite/std/time/format/empty_spec.cc
@@ -653,15 +653,15 @@ wall_cast(const local_time<Dur2>& tp)
using decadays = duration<days::rep, std::ratio_multiply<std::deca, 
days::period>>;
using kilodays = duration<days::rep, std::ratio_multiply<std::kilo, 
days::period>>;

-template<typename CharT, typename Clock>
+template<typename CharT, typename Clock, bool CustomizedOstream>
void
-test_time_point(bool daysAsTime)
+test_time_point()
{
  std::basic_string<CharT> res;

  const auto lt = local_days(2024y/March/22) + 13h + 24min + 54s + 111222333ns;
-  auto strip_time = [daysAsTime](std::basic_string_view<CharT> sv)
-  { return daysAsTime ? sv : sv.substr(0, 10); };
+  auto strip_time = [](std::basic_string_view<CharT> sv)
+  { return CustomizedOstream ? sv.substr(0, 10) : sv; };

  verify( wall_cast<Clock, nanoseconds>(lt),
          WIDEN("2024-03-22 13:24:54.111222333") );
@@ -681,6 +681,19 @@ test_time_point(bool daysAsTime)
          strip_time(WIDEN("2024-03-18 00:00:00")) );
  verify( wall_cast<Clock, kilodays>(lt),
          strip_time(WIDEN("2022-01-08 00:00:00")) );
+
+  if constexpr (!CustomizedOstream)
+  {
+    verify( wall_cast<Clock, duration<double>>(lt),
+           WIDEN("2024-03-22 13:24:54") );
+    verify( wall_cast<Clock, years>(lt),
+           WIDEN("2024-01-01 02:16:48") );
+  }
+  else
+  {
+    test_no_empty_spec<CharT, time_point<Clock, duration<double>>>();
+    test_no_empty_spec<CharT, time_point<Clock, years>>();
+  }
}

template<typename CharT>
@@ -776,20 +789,18 @@ template<typename CharT>
void
test_time_points()
{
-  test_time_point<CharT, local_t>(false);
-  test_time_point<CharT, system_clock>(false);
-  test_time_point<CharT, utc_clock>(true);
-  test_time_point<CharT, tai_clock>(true);
-  test_time_point<CharT, gps_clock>(true);
-  test_time_point<CharT, file_clock>(true);
+  test_time_point<CharT, local_t, true>();
+  test_time_point<CharT, system_clock, true>();
+  test_time_point<CharT, utc_clock, false>();
+  test_time_point<CharT, tai_clock, false>();
+  test_time_point<CharT, gps_clock, false>();
+  test_time_point<CharT, file_clock, false>();
  test_leap_second<CharT>();
#if _GLIBCXX_USE_CXX11_ABI || !_GLIBCXX_USE_DUAL_ABI
  test_zoned_time<CharT>();
#endif
  test_local_time_format<CharT>();

-  test_no_empty_spec<CharT, sys_time<years>>();
-  test_no_empty_spec<CharT, sys_time<duration<float>>>();
}

#if _GLIBCXX_USE_CXX11_ABI || !_GLIBCXX_USE_DUAL_ABI
--
2.51.0



Reply via email to