From 0b2862a1e4cf24797424501a3d13e3e563a90528 Mon Sep 17 00:00:00 2001 From: Barry Revzin Date: Fri, 29 Jul 2022 15:55:16 -0500 Subject: [PATCH] Range formatter (#2983) * Implement range_formatter and format_kind * Attempted gcc 4.8 fix * gcc 4.8 interprets inaccessible as a hard error (instead of... not available) * Attempting to delete set_debug_format. * clang-format * Different implementation of FMT_STATICALLY_WIDEN * Renaming copy_str_range to copy_str. * I guess I need a definition * Forgot to delete these. * Other PR comments. --- include/fmt/core.h | 17 ++- include/fmt/ostream.h | 2 + include/fmt/ranges.h | 262 +++++++++++++++++++++++++++--------------- test/ranges-test.cc | 3 + 4 files changed, 191 insertions(+), 93 deletions(-) diff --git a/include/fmt/core.h b/include/fmt/core.h index c6ebef209dfb..23d2565e7838 100644 --- a/include/fmt/core.h +++ b/include/fmt/core.h @@ -1667,6 +1667,11 @@ auto copy_str(InputIt begin, InputIt end, appender out) -> appender { return out; } +template +FMT_CONSTEXPR auto copy_str(R&& rng, OutputIt out) -> OutputIt { + return detail::copy_str(rng.begin(), rng.end(), out); +} + #if FMT_GCC_VERSION && FMT_GCC_VERSION < 500 // A workaround for gcc 4.8 to make void_t work in a SFINAE context. template struct void_t_impl { using type = void; }; @@ -2824,7 +2829,8 @@ FMT_CONSTEXPR auto parse_float_type_spec(const basic_format_specs& specs, template FMT_CONSTEXPR auto check_cstring_type_spec(presentation_type type, ErrorHandler&& eh = {}) -> bool { - if (type == presentation_type::none || type == presentation_type::string) + if (type == presentation_type::none || type == presentation_type::string || + type == presentation_type::debug) return true; if (type != presentation_type::pointer) eh.on_error("invalid type specifier"); return false; @@ -3086,6 +3092,15 @@ struct formatter::value, + enable_if_t<(U == detail::type::string_type || + U == detail::type::cstring_type || + U == detail::type::char_type), + int> = 0> + FMT_CONSTEXPR void set_debug_format() { + specs_.type = presentation_type::debug; + } + template FMT_CONSTEXPR auto format(const T& val, FormatContext& ctx) const -> decltype(ctx.out()); diff --git a/include/fmt/ostream.h b/include/fmt/ostream.h index 641fd08bfae7..58787495f9e6 100644 --- a/include/fmt/ostream.h +++ b/include/fmt/ostream.h @@ -146,6 +146,8 @@ template struct streamed_view { const T& value; }; // Formats an object of type T that has an overloaded ostream operator<<. template struct basic_ostream_formatter : formatter, Char> { + void set_debug_format() = delete; + template auto format(const T& value, basic_format_context& ctx) const -> OutputIt { diff --git a/include/fmt/ranges.h b/include/fmt/ranges.h index 7b358b710dda..2e5a8e6851a4 100644 --- a/include/fmt/ranges.h +++ b/include/fmt/ranges.h @@ -309,6 +309,16 @@ OutputIt write_range_entry(OutputIt out, const Arg& v) { return write(out, v); } +template struct string_literal { + static constexpr CharT value[sizeof...(C)] = {C...}; + constexpr operator basic_string_view() const { + return {value, sizeof...(C)}; + } +}; + +template +constexpr CharT string_literal::value[sizeof...(C)]; + } // namespace detail template struct is_tuple_like { @@ -326,18 +336,37 @@ struct formatter::value && fmt::is_tuple_formattable::value>> { private: + basic_string_view separator_ = detail::string_literal{}; + basic_string_view opening_bracket_ = + detail::string_literal{}; + basic_string_view closing_bracket_ = + detail::string_literal{}; + // C++11 generic lambda for format(). template struct format_each { template void operator()(const T& v) { - if (i > 0) out = detail::write_delimiter(out); + if (i > 0) out = detail::copy_str(separator, out); out = detail::write_range_entry(out, v); ++i; } int i; typename FormatContext::iterator& out; + basic_string_view separator; }; public: + FMT_CONSTEXPR formatter() {} + + FMT_CONSTEXPR void set_separator(basic_string_view sep) { + separator_ = sep; + } + + FMT_CONSTEXPR void set_brackets(basic_string_view open, + basic_string_view close) { + opening_bracket_ = open; + closing_bracket_ = close; + } + template FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { return ctx.begin(); @@ -347,9 +376,9 @@ struct formatter decltype(ctx.out()) { auto out = ctx.out(); - *out++ = '('; - detail::for_each(values, format_each{0, out}); - *out++ = ')'; + out = detail::copy_str(opening_bracket_, out); + detail::for_each(values, format_each{0, out, separator_}); + out = detail::copy_str(closing_bracket_, out); return out; } }; @@ -357,7 +386,6 @@ struct formatter struct is_range { static constexpr const bool value = detail::is_range_::value && !detail::is_std_string_like::value && - !detail::is_map::value && !std::is_convertible>::value && !std::is_constructible, T>::value; }; @@ -391,56 +419,76 @@ template using maybe_const_range = conditional_t::value, const R, R>; -// is_nonrecursive_range depends on fmt::is_range::value == true. -// It exists to ensure short-circuit evaluation in the constraint of the -// formatter specialization below. A similar approach is used in -// https://wg21.link/p2286. -template -struct is_nonrecursive_range : bool_constant< - !std::is_same, R>::value> {}; - -// is_formattable_delayed depends on is_nonrecursive_range::value == true. -// It exists to ensure short-circuit evaluation in the constraint of the -// formatter specialization below. -template -struct is_formattable_delayed : disjunction< - is_formattable>, Char>, - has_fallback_formatter>, Char>> {}; - } // namespace detail -template -struct formatter< - R, Char, - enable_if_t< - conjunction, - detail::is_nonrecursive_range -// Workaround a bug in MSVC 2015 and earlier. -#if !FMT_MSC_VERSION || FMT_MSC_VERSION > 1900 - , - detail::is_formattable_delayed -#endif - >::value - >> { +template +struct range_formatter; - using range_type = detail::maybe_const_range; - using formatter_type = - detail::range_formatter_type>; - formatter_type underlying_; +template +struct range_formatter< + T, Char, + enable_if_t>, + disjunction, + detail::has_fallback_formatter>>::value>> { + private: + detail::range_formatter_type underlying_; bool custom_specs_ = false; - bool no_brackets_ = false; + basic_string_view separator_ = detail::string_literal{}; + basic_string_view opening_bracket_ = + detail::string_literal{}; + basic_string_view closing_bracket_ = + detail::string_literal{}; + + template + FMT_CONSTEXPR static auto maybe_set_debug_format(U& u, int) + -> decltype(u.set_debug_format()) { + u.set_debug_format(); + } + + template + FMT_CONSTEXPR static void maybe_set_debug_format(U&, ...) {} + + FMT_CONSTEXPR void maybe_set_debug_format() { + maybe_set_debug_format(underlying_, 0); + } + + public: + FMT_CONSTEXPR range_formatter() {} + + FMT_CONSTEXPR auto underlying() -> detail::range_formatter_type& { + return underlying_; + } + + FMT_CONSTEXPR void set_separator(basic_string_view sep) { + separator_ = sep; + } + + FMT_CONSTEXPR void set_brackets(basic_string_view open, + basic_string_view close) { + opening_bracket_ = open; + closing_bracket_ = close; + } template FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { auto it = ctx.begin(); auto end = ctx.end(); - if (it == end || *it == '}') return it; + if (it == end || *it == '}') { + maybe_set_debug_format(); + return it; + } if (*it == 'n') { - no_brackets_ = true; + set_brackets({}, {}); ++it; } + if (*it == '}') { + maybe_set_debug_format(); + return it; + } + if (*it != ':') FMT_THROW(format_error("no other top-level range formatters supported")); @@ -450,75 +498,105 @@ struct formatter< return underlying_.parse(ctx); } - template - auto format(range_type& range, FormatContext& ctx) const - -> decltype(ctx.out()) { - Char prefix = detail::is_set::value ? '{' : '['; - Char postfix = detail::is_set::value ? '}' : ']'; + template + auto format(R&& range, FormatContext& ctx) const -> decltype(ctx.out()) { detail::range_mapper> mapper; auto out = ctx.out(); - if (!no_brackets_) *out++ = prefix; + out = detail::copy_str(opening_bracket_, out); int i = 0; auto it = detail::range_begin(range); auto end = detail::range_end(range); for (; it != end; ++it) { - if (i > 0) out = detail::write_delimiter(out); - if (custom_specs_) { - ctx.advance_to(out); - out = underlying_.format(mapper.map(*it), ctx); - } else { - out = detail::write_range_entry(out, *it); - } + if (i > 0) out = detail::copy_str(separator_, out); + ; + ctx.advance_to(out); + out = underlying_.format(mapper.map(*it), ctx); ++i; } - if (!no_brackets_) *out++ = postfix; + out = detail::copy_str(closing_bracket_, out); return out; } }; -template +enum class range_format { disabled, map, set, sequence, string, debug_string }; + +namespace detail { +template struct range_format_kind_ { + static constexpr auto value = std::is_same, T>::value + ? range_format::disabled + : is_map::value ? range_format::map + : is_set::value ? range_format::set + : range_format::sequence; +}; + +template +struct range_default_formatter; + +template +using range_format_constant = std::integral_constant; + +template +struct range_default_formatter< + K, R, Char, + enable_if_t<(K == range_format::sequence || K == range_format::map || + K == range_format::set)>> { + using range_type = detail::maybe_const_range; + range_formatter, Char> underlying_; + + FMT_CONSTEXPR range_default_formatter() { init(range_format_constant()); } + + FMT_CONSTEXPR void init(range_format_constant) { + underlying_.set_brackets(detail::string_literal{}, + detail::string_literal{}); + } + + FMT_CONSTEXPR void init(range_format_constant) { + underlying_.set_brackets(detail::string_literal{}, + detail::string_literal{}); + underlying_.underlying().set_brackets({}, {}); + underlying_.underlying().set_separator( + detail::string_literal{}); + } + + FMT_CONSTEXPR void init(range_format_constant) {} + + template + FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { + return underlying_.parse(ctx); + } + + template + auto format(range_type& range, FormatContext& ctx) const + -> decltype(ctx.out()) { + return underlying_.format(range, ctx); + } +}; +} // namespace detail + +template +struct range_format_kind + : conditional_t< + is_range::value, detail::range_format_kind_, + std::integral_constant> {}; + +template struct formatter< - T, Char, - enable_if_t + R, Char, + enable_if_t::value != + range_format::disabled> // Workaround a bug in MSVC 2017 and earlier. #if !FMT_MSC_VERSION || FMT_MSC_VERSION >= 1920 , disjunction< - is_formattable, Char>, - detail::has_fallback_formatter, Char> - >, - disjunction< - is_formattable, Char>, - detail::has_fallback_formatter, Char> - > + is_formattable>, + Char>, + detail::has_fallback_formatter< + detail::uncvref_type>, Char>> #endif - >::value - >> { - template - FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) { - return ctx.begin(); - } - - template < - typename FormatContext, typename U, - FMT_ENABLE_IF( - std::is_same::value, - const T, T>>::value)> - auto format(U& map, FormatContext& ctx) const -> decltype(ctx.out()) { - auto out = ctx.out(); - *out++ = '{'; - int i = 0; - for (const auto& item : map) { - if (i > 0) out = detail::write_delimiter(out); - out = detail::write_range_entry(out, item.first); - *out++ = ':'; - *out++ = ' '; - out = detail::write_range_entry(out, item.second); - ++i; - } - *out++ = '}'; - return out; - } + >::value>> + : detail::range_default_formatter::value, R, + Char> { }; template struct tuple_join_view : detail::view { diff --git a/test/ranges-test.cc b/test/ranges-test.cc index 0c39b3843be5..ef960dce59b7 100644 --- a/test/ranges-test.cc +++ b/test/ranges-test.cc @@ -40,6 +40,8 @@ TEST(ranges_test, format_2d_array) { TEST(ranges_test, format_array_of_literals) { const char* arr[] = {"1234", "abcd"}; EXPECT_EQ(fmt::format("{}", arr), "[\"1234\", \"abcd\"]"); + EXPECT_EQ(fmt::format("{:n}", arr), "\"1234\", \"abcd\""); + EXPECT_EQ(fmt::format("{:n:}", arr), "1234, abcd"); } #endif // FMT_RANGES_TEST_ENABLE_C_STYLE_ARRAY @@ -60,6 +62,7 @@ TEST(ranges_test, format_vector2) { TEST(ranges_test, format_map) { auto m = std::map{{"one", 1}, {"two", 2}}; EXPECT_EQ(fmt::format("{}", m), "{\"one\": 1, \"two\": 2}"); + EXPECT_EQ(fmt::format("{:n}", m), "\"one\": 1, \"two\": 2"); } TEST(ranges_test, format_set) {