From c62f5e03c09bf82f1bc06c2139b68f5faf951348 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Neboj=C5=A1a=20Cvetkovi=C4=87?= Date: Sat, 12 Sep 2026 02:57:34 -0600 Subject: [PATCH] Don't require std::locale in chrono.h when FMT_USE_LOCALE is 0 tm_writer takes a locale_ref and the localized flag instead of a std::locale reference, and resolves the locale the same way numeric {:L} does: classic unless localized, otherwise the one passed to the formatting function or the global one. The facet calls are confined to three shims, so with locale support disabled nothing pulls in std::locale. get_locale, which existed only to materialize and own a std::locale, is no longer needed. The std::tm formatter treated a missing locale argument as the classic locale rather than the global one, and since #4935 routes {:L} on weekday and month through it, {:L} without a locale argument ignored the global locale for all calendar types. It now uses the global locale, as numeric {:L} does. Output with an explicit locale is unchanged. With FMT_USE_LOCALE=0, {:L} no longer consults the global locale, matching numeric formatting under that option. An -Os test program loses all 16 of its std::locale and time_put symbols. --- include/fmt/chrono.h | 107 +++++++++++++++++++++++++------------------ test/chrono-test.cc | 4 ++ 2 files changed, 66 insertions(+), 45 deletions(-) diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h index cc04243b686f..276352e765b2 100644 --- a/include/fmt/chrono.h +++ b/include/fmt/chrono.h @@ -407,6 +407,48 @@ auto write(OutputIt out, const std::tm& time, const std::locale& loc, return write_encoded_tm_str(out, string_view(buf.data(), buf.size()), loc); } +#if FMT_USE_LOCALE +// The locale used for localized formatting: the one passed to the formatting +// function or the global locale if none was passed. +inline auto get_locale(locale_ref loc, bool localized) -> std::locale { + if (!localized) return get_classic_locale(); + return loc.get(); +} + +inline auto is_classic_locale(locale_ref loc, bool localized) -> bool { + return !localized || loc.get() == get_classic_locale(); +} + +template +auto write_localized_time(OutputIt out, const std::tm& time, locale_ref loc, + bool localized, char format, char modifier) + -> OutputIt { + return write(out, time, get_locale(loc, localized), format, modifier); +} + +template +auto write_localized_str(OutputIt out, string_view sv, locale_ref loc, + bool localized) -> OutputIt { + return write_tm_str(out, sv, get_locale(loc, localized)); +} +#else +constexpr auto is_classic_locale(locale_ref, bool) -> bool { return true; } + +// Never called because is_classic_locale() is always true. +template +auto write_localized_time(OutputIt out, const std::tm&, locale_ref, bool, char, + char) -> OutputIt { + return out; +} + +// Zone names are ASCII, so there is nothing to transcode. +template +auto write_localized_str(OutputIt out, string_view sv, locale_ref, bool) + -> OutputIt { + return copy(sv.data(), sv.data() + sv.size(), out); +} +#endif // FMT_USE_LOCALE + template using is_similar_arithmetic_type = bool_constant<(std::is_integral::value && std::is_integral::value) || @@ -1045,7 +1087,8 @@ class tm_writer { private: static constexpr int days_per_week = 7; - const std::locale& loc_; + locale_ref loc_; + bool localized_; bool is_classic_; OutputIt out_; const Duration* subsecs_; @@ -1192,7 +1235,7 @@ class tm_writer { template ::value)> void format_tz_name(const T& tm) { if (!tm.tm_zone) FMT_THROW(format_error("no timezone")); - out_ = write_tm_str(out_, tm.tm_zone, loc_); + out_ = write_localized_str(out_, tm.tm_zone, loc_, localized_); } template ::value)> void format_tz_name(const T&) { @@ -1200,14 +1243,16 @@ class tm_writer { } void format_localized(char format, char modifier = 0) { - out_ = write(out_, tm_, loc_, format, modifier); + out_ = write_localized_time(out_, tm_, loc_, localized_, format, + modifier); } public: - tm_writer(const std::locale& loc, OutputIt out, const std::tm& tm, + tm_writer(locale_ref loc, bool localized, OutputIt out, const std::tm& tm, const Duration* subsecs = nullptr) : loc_(loc), - is_classic_(loc_ == get_classic_locale()), + localized_(localized), + is_classic_(is_classic_locale(loc, localized)), out_(out), subsecs_(subsecs), tm_(tm) {} @@ -1583,31 +1628,6 @@ auto format_duration_unit(OutputIt out) -> OutputIt { return out; } -class get_locale { - private: - union { - std::locale locale_; - }; - bool has_locale_ = false; - - public: - inline get_locale(bool localized, locale_ref loc) : has_locale_(localized) { - if (!localized) return; - ignore_unused(loc); - ::new (&locale_) std::locale( -#if FMT_USE_LOCALE - loc.template get() -#endif - ); - } - inline ~get_locale() { - if (has_locale_) locale_.~locale(); - } - inline operator const std::locale&() const { - return has_locale_ ? locale_ : get_classic_locale(); - } -}; - template struct duration_formatter { using iterator = basic_appender; @@ -1702,8 +1722,7 @@ struct duration_formatter { template void format_tm(const tm& time, Callback cb, Args... args) { if (isnan(val)) return write_nan(); - get_locale loc(localized, locale); - auto w = tm_writer_type(loc, out, time); + auto w = tm_writer_type(locale, localized, out, time); (w.*cb)(args...); out = w.out(); } @@ -1919,8 +1938,8 @@ struct formatter : private formatter { auto time = std::tm(); time.tm_wday = static_cast(wd.c_encoding()); if (use_tm_formatter_) return formatter::format(time, ctx); - detail::get_locale loc(false, ctx.locale()); - auto w = detail::tm_writer(loc, ctx.out(), time); + auto w = detail::tm_writer(locale_ref(), false, + ctx.out(), time); w.on_abbr_weekday(); return w.out(); } @@ -1944,8 +1963,8 @@ struct formatter : private formatter { auto time = std::tm(); time.tm_mday = static_cast(static_cast(d)); if (use_tm_formatter_) return formatter::format(time, ctx); - detail::get_locale loc(false, ctx.locale()); - auto w = detail::tm_writer(loc, ctx.out(), time); + auto w = detail::tm_writer(locale_ref(), false, + ctx.out(), time); w.on_day_of_month(detail::numeric_system::standard, detail::pad_type::zero); return w.out(); } @@ -1969,8 +1988,8 @@ struct formatter : private formatter { auto time = std::tm(); time.tm_mon = static_cast(static_cast(m)) - 1; if (use_tm_formatter_) return formatter::format(time, ctx); - detail::get_locale loc(false, ctx.locale()); - auto w = detail::tm_writer(loc, ctx.out(), time); + auto w = detail::tm_writer(locale_ref(), false, + ctx.out(), time); w.on_abbr_month(); return w.out(); } @@ -1994,8 +2013,8 @@ struct formatter : private formatter { auto time = std::tm(); time.tm_year = static_cast(y) - 1900; if (use_tm_formatter_) return formatter::format(time, ctx); - detail::get_locale loc(false, ctx.locale()); - auto w = detail::tm_writer(loc, ctx.out(), time); + auto w = detail::tm_writer(locale_ref(), false, + ctx.out(), time); w.on_year(detail::numeric_system::standard, detail::pad_type::zero); return w.out(); } @@ -2022,8 +2041,8 @@ struct formatter : private formatter { time.tm_mon = static_cast(static_cast(val.month())) - 1; time.tm_mday = static_cast(static_cast(val.day())); if (use_tm_formatter_) return formatter::format(time, ctx); - detail::get_locale loc(true, ctx.locale()); - auto w = detail::tm_writer(loc, ctx.out(), time); + auto w = detail::tm_writer(locale_ref(), false, + ctx.out(), time); w.on_iso_date(); return w.out(); } @@ -2140,10 +2159,8 @@ template struct formatter { detail::handle_dynamic_spec(specs.dynamic_width(), specs.width, width_ref_, ctx); - auto loc_ref = specs.localized() ? ctx.locale() : locale_ref(); - detail::get_locale loc(static_cast(loc_ref), loc_ref); auto w = detail::tm_writer, Char, Duration>( - loc, out, tm, subsecs); + ctx.locale(), specs.localized(), out, tm, subsecs); detail::parse_chrono_format(fmt_.begin(), fmt_.end(), w); return detail::write( ctx.out(), basic_string_view(buf.data(), buf.size()), specs); diff --git a/test/chrono-test.cc b/test/chrono-test.cc index f893ad12c9a0..818ec42fc187 100644 --- a/test/chrono-test.cc +++ b/test/chrono-test.cc @@ -717,6 +717,9 @@ TEST(chrono_test, weekday) { EXPECT_THAT(saturdays, Contains(fmt::format(loc, "{:L}", sat))); EXPECT_THAT(saturdays, Contains(fmt::format(loc, "{:L%a}", sat))); EXPECT_THAT(saturdays, Contains(fmt::format(loc, "{:L%a}", tm))); + // Without an explicit locale the global one is used. + EXPECT_THAT(saturdays, Contains(fmt::format("{:L}", sat))); + EXPECT_THAT(saturdays, Contains(fmt::format("{:L%a}", tm))); } } @@ -1038,5 +1041,6 @@ TEST(chrono_test, year_month_day) { auto months = std::vector{"ene.", "ene"}; EXPECT_THAT(months, Contains(fmt::format(loc, "{:L}", month))); EXPECT_THAT(months, Contains(fmt::format(loc, "{:L%b}", month))); + EXPECT_THAT(months, Contains(fmt::format("{:L}", month))); } }