diff --git a/include/fmt/format.h b/include/fmt/format.h index 7187ed89..fbe37371 100644 --- a/include/fmt/format.h +++ b/include/fmt/format.h @@ -53,7 +53,9 @@ # include // std::signbit # include // std::numeric_limits -# if defined(__GLIBCXX__) && !defined(_GLIBCXX_USE_DUAL_ABI) +# if FMT_USE_CONSTEVAL +# include // std::construct_at +# elif defined(__GLIBCXX__) && !defined(_GLIBCXX_USE_DUAL_ABI) // Workaround for pre gcc 5 libstdc++. # include // std::allocator_traits # endif @@ -888,12 +890,26 @@ FMT_API auto allocate(size_t size) -> void*; template struct allocator : private std::decay { using value_type = T; - auto allocate(size_t n) -> T* { + FMT_CONSTEXPR20 auto allocate(size_t n) -> T* { FMT_ASSERT(n <= max_value() / sizeof(T), ""); +#if FMT_USE_CONSTEVAL + // Use the builtin directly to avoid C++ runtime dependencies at -O0. + if (__builtin_is_constant_evaluated()) + return std::allocator().allocate(n); +#endif return static_cast(detail::allocate(n * sizeof(T))); } - void deallocate(T* p, size_t) { free(p); } + FMT_CONSTEXPR20 void deallocate(T* p, size_t n) { +#if FMT_USE_CONSTEVAL + if (__builtin_is_constant_evaluated()) { + std::allocator().deallocate(p, n); + return; + } +#endif + ignore_unused(n); + free(p); + } constexpr friend auto operator==(allocator, allocator) noexcept -> bool { return true; // All instances of this allocator are equivalent. @@ -960,10 +976,17 @@ class basic_memory_buffer : public detail::buffer { new_capacity = max_of(size, max_size); T* old_data = buf.data(); T* new_data = self.alloc_.allocate(new_capacity); - // Suppress a bogus -Wstringop-overflow in gcc 13.1 (#3481). - detail::assume(buf.size() <= new_capacity); // The following code doesn't throw, so the raw pointer above doesn't leak. - memcpy(new_data, old_data, buf.size() * sizeof(T)); + if (detail::is_constant_evaluated()) { +#if FMT_USE_CONSTEVAL + for (size_t i = 0; i < new_capacity; ++i) std::construct_at(new_data + i); +#endif + for (size_t i = 0; i < buf.size(); ++i) new_data[i] = old_data[i]; + } else { + // Suppress a bogus -Wstringop-overflow in gcc 13.1 (#3481). + detail::assume(buf.size() <= new_capacity); + memcpy(new_data, old_data, buf.size() * sizeof(T)); + } self.set(new_data, new_capacity); // deallocate must not throw according to the standard, but even if it does, // the buffer already uses the new storage and will deallocate it in @@ -3669,10 +3692,17 @@ FMT_CONSTEXPR20 auto format_float(Float value, int precision, : f.assign(converted_value); if (is_predecessor_closer) dragon_flags |= dragon::predecessor_closer; if (fixed) dragon_flags |= dragon::fixed; - // Limit precision to the maximum possible number of significant digits in - // an IEEE754 double because we don't need to generate zeros. - const int max_double_digits = 767; - if (precision > max_double_digits) precision = max_double_digits; + // Keep the fast double path's significant-digit limit. + int max_precision = 767; + if ((dragon_flags & dragon::fixup) != 0) { + // Fixed precision still counts fractional places before fixup. A value + // f * 2^e has at most max(-e, 0) fractional decimal places, and its + // number of significant decimal digits is bounded by the bit length of f + // + |e|. + max_precision = fixed ? (f.e < 0 ? -f.e : 0) + : count_digits<1>(f.f) + (f.e < 0 ? -f.e : f.e); + } + if (precision > max_precision) precision = max_precision; format_dragon(f, dragon_flags, precision, buf, exp); } if (!fixed && !specs.alt()) { diff --git a/test/compile-test.cc b/test/compile-test.cc index c3331273..a5c04ae0 100644 --- a/test/compile-test.cc +++ b/test/compile-test.cc @@ -250,6 +250,18 @@ TEST(compile_test, static_format) { EXPECT_STREQ(result.c_str(), "42"); EXPECT_EQ(result.str(), "42"); } + +TEST(compile_test, high_precision_constexpr_float) { + constexpr auto tiny = []() { + auto value = 1.0; + for (int i = 0; i < 900; ++i) value /= 2; + return value; + }(); + constexpr auto result = FMT_STATIC_FORMAT("{:.1000f}", tiny); + EXPECT_EQ(1002u, result.str().size()); + EXPECT_EQ("43174959877427608893241125591038", + fmt::string_view(result.c_str() + 768, 32)); +} # endif TEST(compile_test, text_and_arg) { diff --git a/test/format-test.cc b/test/format-test.cc index 48f386e6..9a65c94c 100644 --- a/test/format-test.cc +++ b/test/format-test.cc @@ -307,6 +307,30 @@ TEST(memory_buffer_test, ctor) { EXPECT_EQ(123u, buffer.capacity()); } +#if FMT_USE_CONSTEVAL && (!FMT_MSC_VERSION || FMT_MSC_VERSION >= 1940) +template constexpr auto constexpr_buffer_growth() -> bool { + basic_memory_buffer buffer; + buffer.push_back('a'); + buffer.push_back('b'); + buffer.push_back('c'); + buffer.push_back('d'); + buffer.push_back('e'); + buffer.push_back('f'); + return buffer.size() == 6 && buffer[0] == 'a' && buffer[1] == 'b' && + buffer[2] == 'c' && buffer[3] == 'd' && buffer[4] == 'e' && + buffer[5] == 'f'; +} + +TEST(memory_buffer_test, constexpr_grow) { + constexpr auto default_allocator = + constexpr_buffer_growth>(); + constexpr auto standard_allocator = + constexpr_buffer_growth>(); + EXPECT_TRUE(default_allocator); + EXPECT_TRUE(standard_allocator); +} +#endif + using std_allocator = allocator_ref>; TEST(memory_buffer_test, move_ctor_inline_buffer) { @@ -1264,10 +1288,8 @@ TEST(format_test, high_precision_trailing_zeros) { fmt::format("{:.{}f}", -0.5f, 768)); EXPECT_EQ(std::string("-0.0625") + std::string(765, '0'), fmt::format("{:#.{}g}", -0.0625, 768)); - EXPECT_EQ(std::string(" ") + fixed, - fmt::format("{:>772.768f}", 0.0625)); - EXPECT_EQ(std::string(" ") + general, - fmt::format("{:>#772.768g}", 0.0625)); + EXPECT_EQ(std::string(" ") + fixed, fmt::format("{:>772.768f}", 0.0625)); + EXPECT_EQ(std::string(" ") + general, fmt::format("{:>#772.768g}", 0.0625)); } TEST(format_test, high_precision_bounded_output) { @@ -1284,6 +1306,29 @@ TEST(format_test, high_precision_bounded_output) { EXPECT_EQ("0.062500x", fmt::string_view(buffer, 9)); } +TEST(format_test, high_precision_long_double) { + if (std::numeric_limits::max_exponent <= 3000 || + fmt::detail::is_double_double::value) + return; + auto tiny = std::ldexp(1.0L, -3000); + auto large = std::ldexp(1.0L, 3000); + char buffer[3004]; + safe_sprintf(buffer, "%.1000Lf", tiny); + EXPECT_EQ(buffer, fmt::format("{:.1000f}", tiny)); + safe_sprintf(buffer, "%.1000Lf", -tiny); + EXPECT_EQ(buffer, fmt::format("{:.1000f}", -tiny)); + safe_sprintf(buffer, "%.3000Lf", tiny); + EXPECT_EQ(buffer, fmt::format("{:.3000f}", tiny)); + safe_sprintf(buffer, "%#.1000Lg", large); + EXPECT_EQ(buffer, fmt::format("{:#.1000g}", large)); + safe_sprintf(buffer, "%.0Lf", large); + EXPECT_EQ(buffer, fmt::format("{:.0f}", large)); + EXPECT_EQ("0." + std::string(1000, '0'), fmt::format("{:.1000f}", 0.0L)); + EXPECT_EQ("-0." + std::string(1000, '0'), fmt::format("{:.1000f}", -0.0L)); + EXPECT_EQ("0.5" + std::string(999, '0'), fmt::format("{:.1000f}", 0.5L)); + EXPECT_EQ("-0.5" + std::string(999, '0'), fmt::format("{:.1000f}", -0.5L)); +} + TEST(format_test, large_precision) { // Iterator used to abort the actual output. struct throwing_iterator {