From dac753b81e28b38c50d600e13076e1a0061bffb8 Mon Sep 17 00:00:00 2001 From: Alexey Ochapov Date: Sun, 29 Nov 2020 19:59:11 +0300 Subject: [PATCH] Basics of formatting at compile-time based on compile-time API (#2019) --- include/fmt/compile.h | 18 +++++------ include/fmt/core.h | 14 +++++++++ include/fmt/format.h | 70 +++++++++++++++++++++++++++---------------- test/compile-test.cc | 69 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 34 deletions(-) diff --git a/include/fmt/compile.h b/include/fmt/compile.h index 71db2ab2..0f7a9005 100644 --- a/include/fmt/compile.h +++ b/include/fmt/compile.h @@ -394,7 +394,7 @@ template struct text { using char_type = Char; template - OutputIt format(OutputIt out, const Args&...) const { + constexpr OutputIt format(OutputIt out, const Args&...) const { return write(out, data); } }; @@ -413,7 +413,7 @@ template struct code_unit { using char_type = Char; template - OutputIt format(OutputIt out, const Args&...) const { + constexpr OutputIt format(OutputIt out, const Args&...) const { return write(out, value); } }; @@ -426,7 +426,7 @@ template struct field { using char_type = Char; template - OutputIt format(OutputIt out, const Args&... args) const { + constexpr OutputIt format(OutputIt out, const Args&... args) const { // This ensures that the argument type is convertile to `const T&`. const T& arg = get(args...); return write(out, arg); @@ -461,7 +461,7 @@ template struct concat { using char_type = typename L::char_type; template - OutputIt format(OutputIt out, const Args&... args) const { + constexpr OutputIt format(OutputIt out, const Args&... args) const { out = lhs.format(out, args...); return rhs.format(out, args...); } @@ -617,8 +617,8 @@ FMT_INLINE std::basic_string format(const CompiledFormat& cf, template ::value)> -OutputIt format_to(OutputIt out, const CompiledFormat& cf, - const Args&... args) { +constexpr OutputIt format_to(OutputIt out, const CompiledFormat& cf, + const Args&... args) { return cf.format(out, args...); } # endif // __cpp_if_constexpr @@ -654,8 +654,8 @@ FMT_INLINE std::basic_string format(const S&, template ::value)> -OutputIt format_to(OutputIt out, const CompiledFormat& cf, - const Args&... args) { +constexpr OutputIt format_to(OutputIt out, const CompiledFormat& cf, + const Args&... args) { using char_type = typename CompiledFormat::char_type; using context = format_context_t; return detail::cf::vformat_to(out, cf, @@ -664,7 +664,7 @@ OutputIt format_to(OutputIt out, const CompiledFormat& cf, template ::value)> -OutputIt format_to(OutputIt out, const S&, const Args&... args) { +FMT_CONSTEXPR OutputIt format_to(OutputIt out, const S&, const Args&... args) { constexpr auto compiled = detail::compile(S()); return format_to(out, compiled, args...); } diff --git a/include/fmt/core.h b/include/fmt/core.h index d7d2de50..d5ae6bee 100644 --- a/include/fmt/core.h +++ b/include/fmt/core.h @@ -96,6 +96,12 @@ # define FMT_CONSTEXPR_DECL #endif +#if __cplusplus >= 202002L +# define FMT_CONSTEXPR20 constexpr +#else +# define FMT_CONSTEXPR20 inline +#endif + #ifndef FMT_OVERRIDE # if FMT_HAS_FEATURE(cxx_override_control) || \ (FMT_GCC_VERSION >= 408 && FMT_HAS_GXX_CXX11) || FMT_MSC_VER >= 1900 @@ -283,6 +289,14 @@ struct monostate {}; namespace detail { +constexpr bool is_constant_evaluated() FMT_DETECTED_NOEXCEPT { +#ifdef __cpp_lib_is_constant_evaluated + return std::is_constant_evaluated(); +#else + return false; +#endif +} + // A helper function to suppress "conditional expression is constant" warnings. template constexpr T const_check(T value) { return value; } diff --git a/include/fmt/format.h b/include/fmt/format.h index a91578a8..04af5063 100644 --- a/include/fmt/format.h +++ b/include/fmt/format.h @@ -390,7 +390,7 @@ inline buffer_appender reserve(buffer_appender it, size_t n) { return it; } -template inline Iterator& reserve(Iterator& it, size_t) { +template constexpr Iterator& reserve(Iterator& it, size_t) { return it; } @@ -414,7 +414,7 @@ inline std::back_insert_iterator base_iterator( } template -inline Iterator base_iterator(Iterator, Iterator it) { +constexpr Iterator base_iterator(Iterator, Iterator it) { return it; } @@ -587,14 +587,17 @@ using needs_conversion = bool_constant< template ::value)> -OutputIt copy_str(InputIt begin, InputIt end, OutputIt it) { +FMT_CONSTEXPR OutputIt copy_str(InputIt begin, InputIt end, OutputIt it) { while (begin != end) *it++ = *begin++; return it; } template ::value)> -inline OutChar* copy_str(InputIt begin, InputIt end, OutChar* out) { +FMT_CONSTEXPR20 OutChar* copy_str(InputIt begin, InputIt end, OutChar* out) { + if (is_constant_evaluated()) { + return copy_str(begin, end, out); + } return std::uninitialized_copy(begin, end, out); } @@ -951,17 +954,7 @@ FMT_EXTERN template struct basic_data; // This is a struct rather than an alias to avoid shadowing warnings in gcc. struct data : basic_data<> {}; -#ifdef FMT_BUILTIN_CLZLL -// Returns the number of decimal digits in n. Leading zeros are not counted -// except for n == 0 in which case count_digits returns 1. -inline int count_digits(uint64_t n) { - // https://github.com/fmtlib/format-benchmark/blob/master/digits10 - auto t = bsr2log10(FMT_BUILTIN_CLZLL(n | 1) ^ 63); - return t - (n < data::zero_or_powers_of_10_64_new[t]); -} -#else -// Fallback version of count_digits used when __builtin_clz is not available. -inline int count_digits(uint64_t n) { +template FMT_CONSTEXPR int count_digits_fallback(T n) { int count = 1; for (;;) { // Integer division is slow so do it for a group of four digits instead @@ -975,10 +968,25 @@ inline int count_digits(uint64_t n) { count += 4; } } + +#ifdef FMT_BUILTIN_CLZLL +// Returns the number of decimal digits in n. Leading zeros are not counted +// except for n == 0 in which case count_digits returns 1. +FMT_CONSTEXPR20 int count_digits(uint64_t n) { + if (is_constant_evaluated()) { + return count_digits_fallback(n); + } + // https://github.com/fmtlib/format-benchmark/blob/master/digits10 + auto t = bsr2log10(FMT_BUILTIN_CLZLL(n | 1) ^ 63); + return t - (n < data::zero_or_powers_of_10_64_new[t]); +} +#else +// Fallback version of count_digits used when __builtin_clz is not available. +FMT_CONSTEXPR int count_digits(uint64_t n) { return count_digits_fallback(n); } #endif #if FMT_USE_INT128 -inline int count_digits(uint128_t n) { +FMT_CONSTEXPR int count_digits(uint128_t n) { int count = 1; for (;;) { // Integer division is slow so do it for a group of four digits instead @@ -995,7 +1003,7 @@ inline int count_digits(uint128_t n) { #endif // Counts the number of digits in n. BITS = log2(radix). -template inline int count_digits(UInt n) { +template FMT_CONSTEXPR int count_digits(UInt n) { int num_digits = 0; do { ++num_digits; @@ -1015,7 +1023,10 @@ template <> int count_digits<4>(detail::fallback_uintptr n); #ifdef FMT_BUILTIN_CLZ // Optional version of count_digits for better performance on 32-bit platforms. -inline int count_digits(uint32_t n) { +FMT_CONSTEXPR20 int count_digits(uint32_t n) { + if (is_constant_evaluated()) { + return count_digits_fallback(n); + } auto t = bsr2log10(FMT_BUILTIN_CLZ(n | 1) ^ 31); return t - (n < data::zero_or_powers_of_10_32_new[t]); } @@ -1075,11 +1086,20 @@ template struct format_decimal_result { // buffer of specified size. The caller must ensure that the buffer is large // enough. template -inline format_decimal_result format_decimal(Char* out, UInt value, - int size) { +FMT_CONSTEXPR20 format_decimal_result format_decimal(Char* out, + UInt value, + int size) { FMT_ASSERT(size >= count_digits(value), "invalid digit count"); out += size; Char* end = out; + if (is_constant_evaluated()) { + while (value >= 10) { + *--out = static_cast('0' + value % 10); + value /= 10; + } + *--out = static_cast('0' + value); + return {out, end}; + } while (value >= 100) { // Integer division is slow so do it for a group of two digits instead // of for every digit. The idea comes from the talk by Alexandrescu @@ -2048,7 +2068,7 @@ OutputIt write(OutputIt out, string_view value) { } template -OutputIt write(OutputIt out, basic_string_view value) { +FMT_CONSTEXPR OutputIt write(OutputIt out, basic_string_view value) { auto it = reserve(out, value.size()); it = copy_str(value.begin(), value.end(), it); return base_iterator(out, it); @@ -2058,7 +2078,7 @@ template ::value && !std::is_same::value && !std::is_same::value)> -OutputIt write(OutputIt out, T value) { +FMT_CONSTEXPR OutputIt write(OutputIt out, T value) { auto abs_value = static_cast>(value); bool negative = is_negative(value); // Don't do -abs_value since it trips unsigned-integer-overflow sanitizer. @@ -2077,19 +2097,19 @@ OutputIt write(OutputIt out, T value) { } template -OutputIt write(OutputIt out, bool value) { +constexpr OutputIt write(OutputIt out, bool value) { return write(out, string_view(value ? "true" : "false")); } template -OutputIt write(OutputIt out, Char value) { +FMT_CONSTEXPR OutputIt write(OutputIt out, Char value) { auto it = reserve(out, 1); *it++ = value; return base_iterator(out, it); } template -OutputIt write(OutputIt out, const Char* value) { +FMT_CONSTEXPR OutputIt write(OutputIt out, const Char* value) { if (!value) { FMT_THROW(format_error("string pointer is null")); } else { diff --git a/test/compile-test.cc b/test/compile-test.cc index c0dda50c..660f4fb1 100644 --- a/test/compile-test.cc +++ b/test/compile-test.cc @@ -7,6 +7,9 @@ #include #include +#if __cplusplus >= 202002L +# include +#endif // Check that fmt/compile.h compiles with windows.h included before it. #ifdef _WIN32 @@ -171,3 +174,69 @@ TEST(CompileTest, TextAndArg) { EXPECT_EQ("42!", fmt::format(FMT_COMPILE("{}!"), 42)); } #endif + +#if __cplusplus >= 202002L +template struct test_string { + template constexpr bool operator==(const T& rhs) const noexcept { + return (std::string_view(rhs).compare(buffer.data()) == 0); + } + + std::array buffer{}; +}; + +template +consteval auto test_format(auto format, const Args&... args) { + test_string string{}; + fmt::format_to(string.buffer.data(), format, args...); + return string; +} + +TEST(CompileTimeFormattingTest, Bool) { + { + constexpr auto result = test_format<5>(FMT_COMPILE("{}"), true); + EXPECT_EQ(result, "true"); + } + { + constexpr auto result = test_format<6>(FMT_COMPILE("{}"), false); + EXPECT_EQ(result, "false"); + } +} + +TEST(CompileTimeFormattingTest, Integer) { + { + constexpr auto result = test_format<3>(FMT_COMPILE("{}"), 42); + EXPECT_EQ(result, "42"); + } + { + constexpr auto result = test_format<4>(FMT_COMPILE("{}"), 420); + EXPECT_EQ(result, "420"); + } + { + constexpr auto result = test_format<6>(FMT_COMPILE("{} {}"), 42, 42); + EXPECT_EQ(result, "42 42"); + } + { + constexpr auto result = + test_format<6>(FMT_COMPILE("{} {}"), uint32_t{42}, uint64_t{42}); + EXPECT_EQ(result, "42 42"); + } +} + +TEST(CompileTimeFormattingTest, String) { + { + constexpr auto result = test_format<3>(FMT_COMPILE("{}"), "42"); + EXPECT_EQ(result, "42"); + } + { + constexpr auto result = + test_format<17>(FMT_COMPILE("{} is {}"), "The answer", "42"); + EXPECT_EQ(result, "The answer is 42"); + } +} + +TEST(CompileTimeFormattingTest, Combination) { + constexpr auto result = + test_format<18>(FMT_COMPILE("{}, {}, {}"), 420, true, "answer"); + EXPECT_EQ(result, "420, true, answer"); +} +#endif