From 0743b2fe44adc276ce352dda5beb75c418c84bea Mon Sep 17 00:00:00 2001 From: ThePhD Date: Thu, 14 Dec 2023 23:14:22 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(Non-Appending=20)Output=20Ranges?= =?UTF-8?q?=20in=20fmt::format=5F(in)to!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - format_to(some_char_array, ...) will now do the right thing for the most part, and also supports appending to existing ranges (using the same conceptual ideals as as std::ranges::to) - The API for format_to is a bit complicated since we do appending and also pass/support all the existing iterator-based use cases. - format_into is a cleaner API that accepts ranges and output iterators, and does not do appending for those ranges that it recognizes (just fills in the `[begin, end)`). This is necessary to avoid strange behaviors on whether or not a container may or may not meet the appending requirements (e.g., more consistent in generic code). - We lose a lot of compile-time performance with how we're checking things now, so not the best job we could be doing.... - Much of that compile-time performance can be gained back with more rigorous return-style enable_if and such, but that is beyond my work - Actual performance for the calls themselves are only marginally smaller for pointer-based iterators (fmt is, internally, lacking continguous iterator support. Counterpoint; buffers are the only case that matters! (this is a bit of a lie lmaoo)) --- CMakeLists.txt | 3 + include/fmt/color.h | 75 ++++++++ include/fmt/compile.h | 34 ++++ include/fmt/core.h | 388 ++++++++++++++++++++++++++++++++++---- include/fmt/format.h | 48 ++++- include/fmt/xchar.h | 104 ++++++++++ test/CMakeLists.txt | 8 + test/format-test.cc | 4 +- test/output-range-test.cc | 192 +++++++++++++++++++ test/ranges-test.cc | 4 +- 10 files changed, 820 insertions(+), 40 deletions(-) create mode 100644 test/output-range-test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 4b928ae3..ba056511 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -451,3 +451,6 @@ if (FMT_MASTER_PROJECT AND EXISTS ${gitignore}) set(CPACK_RESOURCE_FILE_README ${PROJECT_SOURCE_DIR}/README.md) include(CPack) endif () + +add_executable(scratch main.cpp) +target_link_libraries(scratch PRIVATE fmt::fmt) diff --git a/include/fmt/color.h b/include/fmt/color.h index 34de634c..d21d9898 100644 --- a/include/fmt/color.h +++ b/include/fmt/color.h @@ -538,6 +538,80 @@ inline std::basic_string format(const text_style& ts, const S& format_str, fmt::make_format_args>(args...)); } +#if FMT_OUTPUT_RANGES +/** + Formats a string with the given text_style and writes the output to ``out``. + */ +template || std::output_iterator, Char>)> +auto vformat_to( + Output&& out, const text_style& ts, basic_string_view format_str, + basic_format_args>> args) { + auto&& buf = detail::get_appendable_buffer(std::forward(out)); + detail::vformat_to(buf, ts, format_str, args); + return detail::get_iterator(buf, out); +} + +/** + \rst + Formats arguments with the given text_style, writes the result to the output + iterator or range ``out`` and returns the iterator past the end of the output range. + + **Example**:: + + std::vector out; + fmt::format_to(std::back_inserter(out), + fmt::emphasis::bold | fg(fmt::color::red), "{}", 42); + \endrst +*/ +template > + || std::output_iterator, char_t>) + && detail::is_string::value, + FMT_ENABLE_IF(enable)> +inline auto format_to(Output&& out, const text_style& ts, const S& format_str, + Args&&... args) { + return vformat_to(std::forward(out), ts, detail::to_string_view(format_str), + fmt::make_format_args>>(args...)); +} + +/** + Formats a string with the given text_style and writes the output to ``out``. + */ +template + || std::output_iterator, Char>)> +auto vformat_into( + Output&& out, const text_style& ts, basic_string_view format_str, + basic_format_args>> args) { + auto&& buf = detail::get_buffer(std::forward(out)); + detail::vformat_to(buf, ts, format_str, args); + return detail::get_iterator(buf, out); +} + +/** + \rst + Formats arguments with the given text_style, writes the result to the output + iterator ``out`` and returns the iterator past the end of the output range. + + **Example**:: + + std::vector out; + fmt::format_to(std::back_inserter(out), + fmt::emphasis::bold | fg(fmt::color::red), "{}", 42); + \endrst +*/ +template > + || std::output_iterator, char_t>) + && detail::is_string::value, + FMT_ENABLE_IF(enable)> +inline auto format_into(Output&& out, const text_style& ts, const S& format_str, + Args&&... args) { + return vformat_into(std::forward(out), ts, detail::to_string_view(format_str), + fmt::make_format_args>>(args...)); +} +#else /** Formats a string with the given text_style and writes the output to ``out``. */ @@ -573,6 +647,7 @@ inline auto format_to(OutputIt out, const text_style& ts, const S& format_str, return vformat_to(out, ts, detail::to_string_view(format_str), fmt::make_format_args>>(args...)); } +#endif template struct formatter, Char> : formatter { diff --git a/include/fmt/compile.h b/include/fmt/compile.h index a4c7e495..10a309ed 100644 --- a/include/fmt/compile.h +++ b/include/fmt/compile.h @@ -471,6 +471,39 @@ FMT_INLINE std::basic_string format(const S&, } } +#if FMT_OUTPUT_RANGES +template ::value)> +FMT_CONSTEXPR auto format_to(Output&& out, const S&, Args&&... args) { + constexpr auto compiled = detail::compile(S()); + if constexpr (std::is_same, + detail::unknown_format>()) { + return fmt::format_to( + std::forward(out), + static_cast>(S()), + std::forward(args)...); + } else { + return fmt::format_to(std::forward(out), + compiled, std::forward(args)...); + } +} + +template ::value)> +FMT_CONSTEXPR auto format_into(Output&& out, const S&, Args&&... args) { + constexpr auto compiled = detail::compile(S()); + if constexpr (std::is_same, + detail::unknown_format>()) { + return fmt::format_into( + std::forward(out), + static_cast>(S()), + std::forward(args)...); + } else { + return fmt::format_into(std::forward(out), + compiled, std::forward(args)...); + } +} +#else template ::value)> FMT_CONSTEXPR OutputIt format_to(OutputIt out, const S&, Args&&... args) { @@ -485,6 +518,7 @@ FMT_CONSTEXPR OutputIt format_to(OutputIt out, const S&, Args&&... args) { } } #endif +#endif template ::value)> diff --git a/include/fmt/core.h b/include/fmt/core.h index 50ca0b3a..771a1c00 100644 --- a/include/fmt/core.h +++ b/include/fmt/core.h @@ -16,6 +16,9 @@ #include // std::addressof #include #include +#if defined(__cpp_lib_ranges) && defined(__cpp_if_constexpr) +# include +#endif // The fmt library version in the form major * 10000 + minor * 100 + patch. #define FMT_VERSION 100102 @@ -138,9 +141,10 @@ # endif #endif -// Only on if we know we can do the begin/end range checking properly +// Only on if we know we can do the begin/end range checking properly, +// that we have `if constexpr`, and that we have a std::ranges::subrange type #ifndef FMT_OUTPUT_RANGES -# if FMT_RANGE_CHECKS +# if FMT_RANGE_CHECKS && defined(__cpp_if_constexpr) && defined( __cpp_lib_ranges) # define FMT_OUTPUT_RANGES 1 # else # define FMT_OUTPUT_RANGES 0 @@ -809,8 +813,8 @@ inline auto get_container(std::back_insert_iterator it) return *accessor(it).container; } -template -FMT_CONSTEXPR auto copy_str(InputIt begin, InputIt end, OutputIt out) +template +FMT_CONSTEXPR auto copy_str(InputIt begin, InputSen end, OutputIt out) -> OutputIt { while (begin != end) *out++ = static_cast(*begin++); return out; @@ -818,14 +822,44 @@ FMT_CONSTEXPR auto copy_str(InputIt begin, InputIt end, OutputIt out) template , U>::value&& is_char::value)> + std::is_same, U>::value && is_char::value)> FMT_CONSTEXPR auto copy_str(T* begin, T* end, U* out) -> U* { - if (is_constant_evaluated()) return copy_str(begin, end, out); + if (is_constant_evaluated()) return copy_str(begin, end, out); auto size = to_unsigned(end - begin); if (size > 0) memcpy(out, begin, size * sizeof(U)); return out + size; } +#if FMT_OUTPUT_RANGES + +template +FMT_CONSTEXPR auto copy_str(InputIt begin, InputSen end, OutputIt out, OutputSen out_end) + -> std::ranges::subrange { + if constexpr (std::is_same::value) { + // minor speed optimization + auto out_current = copy_str(std::move(begin), std::move(end), std::move(out)); + return { std::move(out_current), std::move(out_end) }; + } + else { + while (begin != end && out != out_end) *out++ = static_cast(*begin++); + return { std::move(out), std::move(out_end) }; + } +} + +template , U>::value && is_char::value)> +FMT_CONSTEXPR auto copy_str(T* begin, T* end, U* out, U* out_end) -> std::ranges::subrange { + if (is_constant_evaluated()) return copy_str(begin, end, out, out_end); + auto size = to_unsigned(end - begin); + auto out_size = to_unsigned(out_end - out); + if (size > out_size) size = out_size; + if (size > 0) memcpy(out, begin, size * sizeof(U)); + return { out + size, out_end }; +} + +#endif + /** \rst A contiguous memory buffer with an optional growing ability. It is an internal @@ -970,6 +1004,114 @@ class iterator_buffer final : public Traits, public buffer { auto count() const -> size_t { return Traits::count() + this->size(); } }; +#if FMT_OUTPUT_RANGES +// A buffer that writes to an output range when flushed. +template +class range_buffer final : public Traits, public buffer { + private: + using Subrange_ = std::ranges::subrange, std::ranges::sentinel_t>; + Subrange_ out_; + enum { buffer_size = 256 }; + T data_[buffer_size]; + + protected: + FMT_CONSTEXPR20 void grow(size_t) override { + if (this->size() == buffer_size) flush(); + } + + void flush() { + if (std::ranges::empty(out_)) + return; + auto size = this->size(); + this->clear(); + auto it_end = copy_str(data_, data_ + this->limit(size), + std::ranges::begin(out_), std::ranges::end(out_)); + out_ = Subrange_(std::move(it_end).begin(), std::move(it_end).end()); + } + + public: + template + explicit range_buffer(Out&& out, size_t n = buffer_size) + : Traits(n), buffer(data_, 0, buffer_size), out_(std::forward(out)) {} + range_buffer(range_buffer&& other) + : Traits(other), buffer(data_, 0, buffer_size), out_(std::move(other.out_)) {} + ~range_buffer() { flush(); } + + auto out() -> Subrange_ { + flush(); + return out_; + } + auto count() const -> size_t { return Traits::count() + this->size(); } +}; + +template +class range_buffer, T, Traits> final : public Traits, public buffer { + private: + using Subrange_ = std::ranges::subrange, std::ranges::sentinel_t>; + Output& out_; + enum { buffer_size = 256 }; + T data_[buffer_size]; + + protected: + FMT_CONSTEXPR20 void grow(size_t) override { + if (this->size() == buffer_size) flush(); + } + + void flush() { + auto size = this->size(); + this->clear(); + copy_str(data_, data_ + this->limit(size), std::back_inserter(out_)); + } + + public: + explicit range_buffer(Output& out, size_t n = buffer_size) + : Traits(n), buffer(data_, 0, buffer_size), out_(out) {} + range_buffer(range_buffer&& other) + : Traits(other), buffer(data_, 0, buffer_size), out_(std::move(other.out_)) {} + ~range_buffer() { flush(); } + + auto out() -> Subrange_ { + flush(); + return Subrange_(out_.end(), out_.end()); + } + auto count() const -> size_t { return Traits::count() + this->size(); } +}; + +template +class range_buffer, T, Traits> final : public Traits, public buffer { + private: + using Subrange_ = std::ranges::subrange, std::ranges::sentinel_t>; + Output& out_; + enum { buffer_size = 256 }; + T data_[buffer_size]; + + protected: + FMT_CONSTEXPR20 void grow(size_t) override { + if (this->size() == buffer_size) flush(); + } + + void flush() { + auto size = this->size(); + this->clear(); + copy_str(data_, data_ + this->limit(size), std::inserter(out_, out_.end())); + } + + public: + explicit range_buffer(Output& out, size_t n = buffer_size) + : Traits(n), buffer(data_, 0, buffer_size), out_(out) {} + range_buffer(range_buffer&& other) + : Traits(other), buffer(data_, 0, buffer_size), out_(std::move(other.out_)) {} + ~range_buffer() { flush(); } + + auto out() -> Subrange_ { + flush(); + return Subrange_(out_.end(), out_.end()); + } + auto count() const -> size_t { return Traits::count() + this->size(); } +}; +#endif + +// A buffer that writes to an output iterator when flushed. template class iterator_buffer final : public fixed_buffer_traits, @@ -1149,26 +1291,6 @@ template using buffer_appender = conditional_t::value, appender, std::back_insert_iterator>>; -// Maps an output iterator to a buffer. -template -auto get_buffer(OutputIt out) -> iterator_buffer { - return iterator_buffer(out); -} -template , Buf>::value)> -auto get_buffer(std::back_insert_iterator out) -> buffer& { - return get_container(out); -} - -template -FMT_INLINE auto get_iterator(Buf& buf, OutputIt) -> decltype(buf.out()) { - return buf.out(); -} -template -auto get_iterator(buffer&, OutputIt out) -> OutputIt { - return out; -} - struct view {}; template struct named_arg : view { @@ -1643,6 +1765,95 @@ template struct is_back_insert_iterator> : std::true_type {}; +#if FMT_OUTPUT_RANGES +template +struct is_container_push_backable : std::false_type {}; + +template +struct is_container_push_backable< + Container, T, + void_t().push_back(std::declval()))> +> : std::true_type {}; + +template +struct is_container_insertable : std::false_type {}; + +template +struct is_container_insertable< + Container, T, + void_t().insert( + std::declval>(), + std::declval() + ))> +> : std::integral_constant::value +> {}; +#endif + +// Maps an output iterator to a buffer. +#if FMT_OUTPUT_RANGES +template +decltype(auto) get_buffer(Output&& out) { + if constexpr (std::ranges::output_range) { + return range_buffer, T>(std::forward(out)); + } + else if constexpr (is_back_insert_iterator>::value) { + using Container_ = typename remove_cvref_t::container_type; + if constexpr (std::is_base_of, Container_>::value) { + return get_container(out); + } + else { + return iterator_buffer, T>(std::forward(out)); + } + } + else { + return iterator_buffer, T>(std::forward(out)); + } +} + +template +decltype(auto) get_appendable_buffer(Output&& out) { + if constexpr (std::ranges::output_range) { + if constexpr (is_container_insertable::value) { + // appendable container: give a back_inserter-based buffer + if constexpr (is_container_push_backable::value) { + using Iterator_ = std::back_insert_iterator>; + return range_buffer(std::forward(out)); + } + else { + using Iterator_ = std::insert_iterator>; + return range_buffer(std::forward(out)); + } + } + else { + return range_buffer, T>(std::forward(out)); + } + } + else { + return get_buffer(std::forward(out)); + } +} +#else +template +auto get_buffer(OutputIt out) -> iterator_buffer { + return iterator_buffer(out); +} +template , Buf>::value)> +auto get_buffer(std::back_insert_iterator out) -> buffer& { + return get_container(out); +} +#endif + +template +FMT_INLINE auto get_iterator(Buf& buf, Output&&) -> decltype(buf.out()) { + return buf.out(); +} +template +auto get_iterator(buffer&, Output&& out) { + return out; +} + // A type-erased reference to an std::locale to avoid a heavy include. class locale_ref { private: @@ -1716,14 +1927,38 @@ FMT_CONSTEXPR inline auto make_arg(T& val) -> basic_format_arg { template ::value> struct out_storage { + using iterator = remove_cvref_t; + using range = void; Out out_; + + template , out_storage>::value)> + FMT_CONSTEXPR out_storage(Value&& value) : out_(std::forward(value)) {} + + out_storage(out_storage&&) = default; + out_storage(const out_storage&) = default; + out_storage& operator=(out_storage&&) = default; + out_storage& operator=(const out_storage&) = default; }; +#if FMT_OUTPUT_RANGES template struct out_storage { - decltype(range_begin(std::declval())) out_; - decltype(range_end(std::declval())) out_last_; + using iterator = std::ranges::iterator_t; + using sentinel = std::ranges::sentinel_t; + using range = std::ranges::subrange; + range out_; + + template , out_storage>::value)> + FMT_CONSTEXPR out_storage(Value&& value) : out_(std::forward(value)) {} + + out_storage(out_storage&&) = default; + out_storage(const out_storage&) = default; + out_storage& operator=(out_storage&&) = default; + out_storage& operator=(const out_storage&) = default; }; +#endif } // namespace detail FMT_BEGIN_EXPORT @@ -1834,12 +2069,18 @@ FMT_CONSTEXPR FMT_INLINE auto visit_format_arg( // Formatting context. template class basic_format_context { private: - OutputIt out_; + using Storage_ = detail::out_storage; + Storage_ out_storage_; basic_format_args args_; detail::locale_ref loc_; + using ItOrRange_ = conditional_t< + std::is_void::value, + typename Storage_::iterator, + typename Storage_::range>; public: - using iterator = OutputIt; + using iterator = typename Storage_::iterator; + using range = typename Storage_::range; using format_arg = basic_format_arg; using format_args = basic_format_args; using parse_context_type = basic_format_parse_context; @@ -1857,7 +2098,7 @@ template class basic_format_context { */ constexpr basic_format_context(OutputIt out, format_args ctx_args, detail::locale_ref loc = {}) - : out_(out), args_(ctx_args), loc_(loc) {} + : out_storage_(out), args_(ctx_args), loc_(loc) {} constexpr auto arg(int id) const -> format_arg { return args_.get(id); } FMT_CONSTEXPR auto arg(basic_string_view name) -> format_arg { @@ -1871,12 +2112,17 @@ template class basic_format_context { FMT_CONSTEXPR auto error_handler() -> detail::error_handler { return {}; } void on_error(const char* message) { error_handler().on_error(message); } - // Returns an iterator to the beginning of the output range. - FMT_CONSTEXPR auto out() -> iterator { return out_; } + // Returns an iterator to the beginning of the + // output range, or the whole output range. + FMT_CONSTEXPR auto out() -> ItOrRange_ { return out_storage_.out_; } // Advances the begin iterator to ``it``. - void advance_to(iterator it) { - if (!detail::is_back_insert_iterator()) out_ = it; + void advance_to(ItOrRange_ it_or_range) { +#if FMT_OUTPUT_RANGES + if (!detail::is_back_insert_iterator()) out_storage_.out_ = std::move(it_or_range); +#else + if (!detail::is_back_insert_iterator()) out_storage_.out_ = it_or_range; +#endif } FMT_CONSTEXPR auto locale() -> detail::locale_ref { return loc_; } @@ -2930,6 +3176,30 @@ FMT_NODISCARD FMT_INLINE auto format(format_string fmt, T&&... args) return vformat(fmt, fmt::make_format_args(args...)); } +#if FMT_OUTPUT_RANGES +/** Formats a string and writes the output to ``out``, appending + rather than directly writing if it is a typical insertable + contianer type. */ +template + || std::output_iterator, char>)> +auto vformat_to(Output&& out, string_view fmt, format_args args) { + auto&& buf = detail::get_appendable_buffer(std::forward(out)); + detail::vformat_to(buf, fmt, args, {}); + return detail::get_iterator(buf, out); +} + +/** Formats a string and writes the output to ``out``, writing + directly into the range [begin, end). */ +template + || std::output_iterator, char>)> +auto vformat_into(Output&& out, string_view fmt, format_args args) { + auto&& buf = detail::get_buffer(std::forward(out)); + detail::vformat_to(buf, fmt, args, {}); + return detail::get_iterator(buf, out); +} +#else /** Formats a string and writes the output to ``out``. */ template ::value)> @@ -2938,7 +3208,54 @@ auto vformat_to(OutputIt out, string_view fmt, format_args args) -> OutputIt { detail::vformat_to(buf, fmt, args, {}); return detail::get_iterator(buf, out); } +#endif +#if FMT_OUTPUT_RANGES +/** + \rst + Formats ``args`` according to specifications in ``fmt``, writes the result to + the output range or iterator ``out`` and returns the subrange past the end of + the output range. `format_to` will write to the end of a container (if it is an + insertable container). `format_to` does not append a terminating null character. + + **Example**:: + + auto out = std::vector(); + fmt::format_to(out, "{}", 42); + + **Example**:: + + auto out = std::vector(); + fmt::format_to(std::back_inserter(out), "{}", 42); + \endrst + */ +template + || std::output_iterator, char>)> +FMT_INLINE auto format_to(Output&& out, format_string fmt, T&&... args) { + return vformat_to(std::forward(out), fmt, fmt::make_format_args(args...)); +} + +/** + \rst + Formats ``args`` according to specifications in ``fmt``, writes the result into + the output range or iterator ``out`` and returns the subrange past the end of + the output range. `format_into` does not append into a contianer and only writes + into it. `format_into` does not append or write a terminating null character. + + **Example**:: + + auto out = std::vector(2, '\0'); + fmt::format_into(out, "{}", 42); + \endrst + */ +template + || std::output_iterator, char>)> +FMT_INLINE auto format_into(Output&& out, format_string fmt, T&&... args) { + return vformat_into(std::forward(out), fmt, fmt::make_format_args(args...)); +} +#else /** \rst Formats ``args`` according to specifications in ``fmt``, writes the result to @@ -2957,6 +3274,7 @@ FMT_INLINE auto format_to(OutputIt out, format_string fmt, T&&... args) -> OutputIt { return vformat_to(out, fmt, fmt::make_format_args(args...)); } +#endif template struct format_to_n_result { /** Iterator past the end of the output range. */ diff --git a/include/fmt/format.h b/include/fmt/format.h index fa76aa11..d9dd7ea2 100644 --- a/include/fmt/format.h +++ b/include/fmt/format.h @@ -2326,7 +2326,7 @@ class counting_iterator { FMT_UNCHECKED_ITERATOR(counting_iterator); struct value_type { - template FMT_CONSTEXPR void operator=(const T&) {} + template FMT_CONSTEXPR void operator=(const T&) const {} }; FMT_CONSTEXPR counting_iterator() : count_(0) {} @@ -4541,6 +4541,51 @@ inline auto format(const Locale& loc, format_string fmt, T&&... args) return fmt::vformat(loc, string_view(fmt), fmt::make_format_args(args...)); } +#if FMT_OUTPUT_RANGES +template + || std::output_iterator) + && detail::is_locale::value)> +auto vformat_to(Output&& out, const Locale& loc, string_view fmt, + format_args args) { + auto&& buf = detail::get_appendable_buffer(std::forward(out)); + detail::vformat_to(buf, fmt, args, detail::locale_ref(loc)); + return detail::get_iterator(buf, out); +} + +template + || std::output_iterator) + && detail::is_locale::value)> +auto vformat_into(Output&& out, const Locale& loc, string_view fmt, + format_args args) { + auto&& buf = detail::get_buffer(std::forward(out)); + detail::vformat_to(buf, fmt, args, detail::locale_ref(loc)); + return detail::get_iterator(buf, out); +} + +template + || std::output_iterator, char>) + && detail::is_locale::value)> +FMT_INLINE auto format_to(Output&& out, const Locale& loc, + format_string fmt, T&&... args) { + return vformat_to(std::forward(out), loc, fmt, fmt::make_format_args(args...)); +} + +template + || std::output_iterator, char>) + && detail::is_locale::value)> +FMT_INLINE auto format_into(Output&& out, const Locale& loc, + format_string fmt, T&&... args) { + return vformat_into(std::forward(out), loc, fmt, fmt::make_format_args(args...)); +} +#else template ::value&& detail::is_locale::value)> @@ -4559,6 +4604,7 @@ FMT_INLINE auto format_to(OutputIt out, const Locale& loc, format_string fmt, T&&... args) -> OutputIt { return vformat_to(out, loc, fmt, fmt::make_format_args(args...)); } +#endif template ::value)> diff --git a/include/fmt/xchar.h b/include/fmt/xchar.h index 9a7aa4d7..e45293ff 100644 --- a/include/fmt/xchar.h +++ b/include/fmt/xchar.h @@ -138,6 +138,109 @@ inline auto format(const Locale& loc, const S& format_str, T&&... args) fmt::make_format_args>(args...)); } +#if FMT_OUTPUT_RANGES +template , + FMT_ENABLE_IF((std::ranges::output_range + || std::output_iterator, Char>) + && detail::is_exotic_char::value)> +auto vformat_to(Output&& out, const S& format_str, + basic_format_args>> args) +{ + auto&& buf = detail::get_appendable_buffer(std::forward(out)); + detail::vformat_to(buf, detail::to_string_view(format_str), args); + return detail::get_iterator(buf, out); +} + +template , + FMT_ENABLE_IF((std::ranges::output_range + || std::output_iterator, Char>) + && detail::is_exotic_char::value)> +auto vformat_into(Output&& out, const S& format_str, + basic_format_args>> args) +{ + auto&& buf = detail::get_buffer(std::forward(out)); + detail::vformat_to(buf, detail::to_string_view(format_str), args); + return detail::get_iterator(buf, out); +} + +template , + FMT_ENABLE_IF((std::ranges::output_range + || std::output_iterator, Char>) + && detail::is_exotic_char::value)> +inline auto format_to(Output&& out, const S& fmt, T&&... args) { + return vformat_to(std::forward(out), detail::to_string_view(fmt), + fmt::make_format_args>(args...)); +} + +template , + FMT_ENABLE_IF((std::ranges::output_range + || std::output_iterator, Char>) + && detail::is_exotic_char::value)> +inline auto format_into(Output&& out, const S& fmt, T&&... args) { + return vformat_into(std::forward(out), detail::to_string_view(fmt), + fmt::make_format_args>(args...)); +} + +template , + FMT_ENABLE_IF((std::ranges::output_range + || std::output_iterator, Char>) + && detail::is_locale::value + && detail::is_exotic_char::value)> +inline auto vformat_to( + Output&& out, const Locale& loc, const S& format_str, + basic_format_args>> args) { + auto&& buf = detail::get_appendable_buffer(std::forward(out)); + vformat_to(buf, detail::to_string_view(format_str), args, + detail::locale_ref(loc)); + return detail::get_iterator(buf, out); +} + +template , + FMT_ENABLE_IF((std::ranges::output_range + || std::output_iterator, Char>) + && detail::is_locale::value + && detail::is_exotic_char::value)> +inline auto vformat_into( + Output&& out, const Locale& loc, const S& format_str, + basic_format_args>> args) { + auto&& buf = detail::get_buffer(std::forward(out)); + vformat_to(buf, detail::to_string_view(format_str), args, + detail::locale_ref(loc)); + return detail::get_iterator(buf, out); +} + +template < + typename Output, typename Locale, typename S, typename... T, + typename Char = char_t, + bool enable = (std::ranges::output_range + || std::output_iterator, Char>) + && detail::is_locale::value + && detail::is_exotic_char::value, + FMT_ENABLE_IF(enable)> +inline auto format_to(Output&& out, const Locale& loc, const S& format_str, + T&&... args) { + return vformat_to(std::forward(out), loc, detail::to_string_view(format_str), + fmt::make_format_args>(args...)); +} + +template < + typename Output, typename Locale, typename S, typename... T, + typename Char = char_t, + bool enable = (std::ranges::output_range + || std::output_iterator, Char>) + && detail::is_locale::value + && detail::is_exotic_char::value, + FMT_ENABLE_IF(enable)> +inline auto format_into(Output&& out, const Locale& loc, const S& format_str, + T&&... args) { + return vformat_into(std::forward(out), loc, detail::to_string_view(format_str), + fmt::make_format_args>(args...)); +} +#else template , FMT_ENABLE_IF(detail::is_output_iterator::value&& detail::is_exotic_char::value)> @@ -183,6 +286,7 @@ inline auto format_to(OutputIt out, const Locale& loc, const S& format_str, return vformat_to(out, loc, detail::to_string_view(format_str), fmt::make_format_args>(args...)); } +#endif template ::value&& diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ec97ac29..0bf2fe35 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -73,6 +73,14 @@ if (NOT (MSVC AND BUILD_SHARED_LIBS)) add_fmt_test(format-impl-test HEADER_ONLY header-only-test.cc) endif () add_fmt_test(ostream-test) +if (CMAKE_CXX_STANDARD GREATER_EQUAL 20) + add_fmt_test(output-range-test) + target_compile_features(output-range-test PRIVATE cxx_std_20) + if (MSVC) + # Without this option, MSVC returns 199711L for the __cplusplus macro. + target_compile_options(output-range-test PRIVATE /Zc:__cplusplus) + endif() +endif() add_fmt_test(compile-test) add_fmt_test(compile-fp-test HEADER_ONLY) if (MSVC) diff --git a/test/format-test.cc b/test/format-test.cc index e967ed32..7d7cb534 100644 --- a/test/format-test.cc +++ b/test/format-test.cc @@ -1696,8 +1696,8 @@ TEST(format_test, format_custom) { TEST(format_test, format_to_custom) { char buf[10] = {}; - auto end = fmt::format_to(buf, "{}", Answer()); - EXPECT_EQ(end, buf + 2); + auto result_range = fmt::format_to(buf, "{}", Answer()); + EXPECT_EQ(result_range.begin(), buf + 2); EXPECT_STREQ(buf, "42"); } diff --git a/test/output-range-test.cc b/test/output-range-test.cc new file mode 100644 index 00000000..245c361a --- /dev/null +++ b/test/output-range-test.cc @@ -0,0 +1,192 @@ +// Formatting library for C++ - std::ostream support tests +// +// Copyright (c) 2012 - present, Victor Zverovich +// All rights reserved. +// +// For the license information refer to format.h. + +#include "fmt/format.h" + +#if FMT_OUTPUT_RANGES + +#include "gmock/gmock.h" +#include "gtest-extra.h" +#include "util.h" + +#include +#include +#include +#include +#include + +TEST(output_range_c_array_char_test, format_into) { + char buffer[4]; + buffer[3] = 'x'; + auto result = fmt::format_into(buffer, "{}", "abc"); + EXPECT_EQ(buffer + 3, result.begin()); + EXPECT_EQ("abc", fmt::string_view(buffer, 3)); + EXPECT_EQ("abcx", fmt::string_view(buffer, 4)); + result = fmt::format_into(buffer, "x{}y", "abc"); + EXPECT_EQ(buffer + 4, result.begin()); + EXPECT_EQ("xabc", fmt::string_view(buffer, 4)); +} + +TEST(output_range_array_char_test, format_into) { + std::array buffer; + buffer[3] = 'x'; + auto result = fmt::format_into(buffer, "{}", "abc"); + EXPECT_EQ(buffer.begin() + 3, result.begin()); + EXPECT_EQ("abc", fmt::string_view(buffer.data(), 3)); + EXPECT_EQ("abcx", fmt::string_view(buffer.data(), 4)); + result = fmt::format_into(buffer, "x{}y", "abc"); + EXPECT_EQ(buffer.begin() + 4, result.begin()); + EXPECT_EQ("xabc", fmt::string_view(buffer.data(), 4)); +} + +TEST(output_range_span_array_char_test, format_into) { + std::array storage; + std::span buffer(storage); + buffer[3] = 'x'; + auto result = fmt::format_into(buffer, "{}", "abc"); + EXPECT_EQ(buffer.begin() + 3, result.begin()); + EXPECT_EQ("abc", fmt::string_view(buffer.data(), 3)); + EXPECT_EQ("abcx", fmt::string_view(buffer.data(), 4)); + result = fmt::format_into(buffer, "x{}y", "abc"); + EXPECT_EQ(buffer.begin() + 4, result.begin()); + EXPECT_EQ("xabc", fmt::string_view(buffer.data(), 4)); +} + +TEST(output_range_vector_char_test, format_into) { + std::vector buffer(4, '\0'); + buffer[3] = 'x'; + auto result = fmt::format_into(buffer, "{}", "abc"); + EXPECT_EQ(buffer.begin() + 3, result.begin()); + EXPECT_EQ("abc", fmt::string_view(buffer.data(), 3)); + EXPECT_EQ("abcx", fmt::string_view(buffer.data(), 4)); + result = fmt::format_into(buffer, "x{}y", "abc"); + EXPECT_EQ(buffer.begin() + 4, result.begin()); + EXPECT_EQ("xabc", fmt::string_view(buffer.data(), 4)); +} + +TEST(output_range_list_char_test, format_into) { + std::list buffer(4, '\0'); + using subrng = std::ranges::subrange; + auto fourth_it = std::ranges::next(buffer.begin(), 3); + auto end = buffer.end(); + subrng first3_subrange(buffer.begin(), fourth_it); + *fourth_it = 'x'; + auto result = fmt::format_into(buffer, "{}", "abc"); + EXPECT_EQ(fourth_it, result.begin()); + EXPECT_TRUE(std::ranges::equal(fmt::string_view("abc"), first3_subrange)); + EXPECT_TRUE(std::ranges::equal(fmt::string_view("abcx"), buffer)); + result = fmt::format_into(buffer, "x{}y", "abc"); + EXPECT_EQ(end, result.begin()); + EXPECT_TRUE(std::ranges::equal(fmt::string_view("xabc"), buffer)); +} + +TEST(output_range_deque_char_test, format_into) { + std::deque buffer(4, '\0'); + using subrng = std::ranges::subrange; + auto fourth_it = std::ranges::next(buffer.begin(), 3); + auto end = buffer.end(); + subrng first3_subrange(buffer.begin(), fourth_it); + *fourth_it = 'x'; + auto result = fmt::format_into(buffer, "{}", "abc"); + EXPECT_EQ(fourth_it, result.begin()); + EXPECT_TRUE(std::ranges::equal(fmt::string_view("abc"), first3_subrange)); + EXPECT_TRUE(std::ranges::equal(fmt::string_view("abcx"), buffer)); + result = fmt::format_into(buffer, "x{}y", "abc"); + EXPECT_EQ(end, result.begin()); + EXPECT_TRUE(std::ranges::equal(fmt::string_view("xabc"), buffer)); +} + +TEST(output_range_c_array_char_test, format_to) { + char buffer[4]; + buffer[3] = 'x'; + auto result = fmt::format_to(buffer, "{}", "abc"); + EXPECT_EQ(buffer + 3, result.begin()); + EXPECT_EQ("abc", fmt::string_view(buffer, 3)); + EXPECT_EQ("abcx", fmt::string_view(buffer, 4)); + result = fmt::format_to(buffer, "x{}y", "abc"); + EXPECT_EQ(buffer + 4, result.begin()); + EXPECT_EQ("xabc", fmt::string_view(buffer, 4)); +} + +TEST(output_range_array_char_test, format_to) { + std::array buffer; + buffer[3] = 'x'; + auto result = fmt::format_to(buffer, "{}", "abc"); + EXPECT_EQ(buffer.begin() + 3, result.begin()); + EXPECT_EQ("abc", fmt::string_view(buffer.data(), 3)); + EXPECT_EQ("abcx", fmt::string_view(buffer.data(), 4)); + result = fmt::format_to(buffer, "x{}y", "abc"); + EXPECT_EQ(buffer.begin() + 4, result.begin()); + EXPECT_EQ("xabc", fmt::string_view(buffer.data(), 4)); +} + +TEST(output_range_span_array_char_test, format_to) { + std::array storage; + std::span buffer(storage); + buffer[3] = 'x'; + auto result = fmt::format_to(buffer, "{}", "abc"); + EXPECT_EQ(buffer.begin() + 3, result.begin()); + EXPECT_EQ("abc", fmt::string_view(buffer.data(), 3)); + EXPECT_EQ("abcx", fmt::string_view(buffer.data(), 4)); + result = fmt::format_to(buffer, "x{}y", "abc"); + EXPECT_EQ(buffer.begin() + 4, result.begin()); + EXPECT_EQ("xabc", fmt::string_view(buffer.data(), 4)); +} + +TEST(output_range_vector_char_test, format_to) { + std::vector buffer{}; + auto result = fmt::format_to(buffer, "{}", "abc"); + EXPECT_EQ(buffer.begin() + 3, result.begin()); + EXPECT_EQ("abc", fmt::string_view(buffer.data(), 3)); + result = fmt::format_to(buffer, "x{}y", "abc"); + EXPECT_EQ(buffer.begin() + 8, result.begin()); + EXPECT_EQ(buffer.begin() + buffer.size(), result.begin()); + EXPECT_EQ(buffer.end(), result.begin()); + EXPECT_EQ("abcxabcy", fmt::string_view(buffer.data(), 8)); +} + +TEST(output_range_basic_string_char_test, format_to) { + std::string buffer{}; + auto result = fmt::format_to(buffer, "{}", "abc"); + EXPECT_EQ(buffer.begin() + 3, result.begin()); + EXPECT_EQ("abc", buffer); + result = fmt::format_to(buffer, "x{}y", "abc"); + EXPECT_EQ(buffer.begin() + 8, result.begin()); + EXPECT_EQ(buffer.begin() + buffer.size(), result.begin()); + EXPECT_EQ(buffer.end(), result.begin()); + EXPECT_EQ("abcxabcy", buffer); +} + +TEST(output_range_list_char_test, format_to) { + std::list buffer{}; + auto result = fmt::format_to(buffer, "{}", "abc"); + EXPECT_EQ(buffer.end(), result.begin()); + EXPECT_EQ(std::ranges::next(buffer.begin(), 3), result.begin()); + EXPECT_EQ(std::ranges::next(buffer.begin(), buffer.size()), result.begin()); + EXPECT_TRUE(std::ranges::equal(fmt::string_view("abc"), buffer)); + result = fmt::format_to(buffer, "x{}y", "abc"); + EXPECT_EQ(buffer.end(), result.begin()); + EXPECT_EQ(std::ranges::next(buffer.begin(), 8), result.begin()); + EXPECT_EQ(std::ranges::next(buffer.begin(), buffer.size()), result.begin()); + EXPECT_TRUE(std::ranges::equal(fmt::string_view("abcxabcy"), buffer)); +} + +TEST(output_range_deque_char_test, format_to) { + std::deque buffer{}; + auto result = fmt::format_to(buffer, "{}", "abc"); + EXPECT_EQ(buffer.end(), result.begin()); + EXPECT_EQ(std::ranges::next(buffer.begin(), 3), result.begin()); + EXPECT_EQ(std::ranges::next(buffer.begin(), buffer.size()), result.begin()); + EXPECT_TRUE(std::ranges::equal(fmt::string_view("abc"), buffer)); + result = fmt::format_to(buffer, "x{}y", "abc"); + EXPECT_EQ(buffer.end(), result.begin()); + EXPECT_EQ(std::ranges::next(buffer.begin(), 8), result.begin()); + EXPECT_EQ(std::ranges::next(buffer.begin(), buffer.size()), result.begin()); + EXPECT_TRUE(std::ranges::equal(fmt::string_view("abcxabcy"), buffer)); +} + +#endif diff --git a/test/ranges-test.cc b/test/ranges-test.cc index ba5c464d..0ceef775 100644 --- a/test/ranges-test.cc +++ b/test/ranges-test.cc @@ -205,8 +205,8 @@ TEST(ranges_test, format_struct) { TEST(ranges_test, format_to) { char buf[10]; - auto end = fmt::format_to(buf, "{}", std::vector{1, 2, 3}); - *end = '\0'; + auto result_range = fmt::format_to(buf, "{}", std::vector{1, 2, 3}); + *result_range.begin() = '\0'; EXPECT_STREQ(buf, "[1, 2, 3]"); }