From 7a1a67ff9332c2478151de10a6c5fa08867ea7dd Mon Sep 17 00:00:00 2001 From: vsol Date: Mon, 9 Mar 2020 20:50:52 +0300 Subject: [PATCH] Dynamic arguments storage. Implementation of enhancement from issue #1170. First try... --- include/fmt/core.h | 26 +++++ include/fmt/dyn-args.h | 199 +++++++++++++++++++++++++++++++++++ test/CMakeLists.txt | 1 + test/format-dyn-args-test.cc | 124 ++++++++++++++++++++++ 4 files changed, 350 insertions(+) create mode 100644 include/fmt/dyn-args.h create mode 100644 test/format-dyn-args-test.cc diff --git a/include/fmt/core.h b/include/fmt/core.h index 0e6422d9..847cf11b 100644 --- a/include/fmt/core.h +++ b/include/fmt/core.h @@ -1005,6 +1005,10 @@ template class basic_format_arg { friend FMT_CONSTEXPR basic_format_arg internal::make_arg( const T& value); + template + friend FMT_CONSTEXPR basic_format_arg internal::make_arg( + const named_arg_base& value); + template friend FMT_CONSTEXPR auto visit_format_arg(Visitor&& vis, const basic_format_arg& arg) @@ -1177,6 +1181,15 @@ template make_arg(const T& value) { return make_arg(value); } + +template +FMT_CONSTEXPR basic_format_arg make_arg( + const named_arg_base& value) { + basic_format_arg arg; + arg.type_ = type::named_arg_type; + arg.value_ = value; + return arg; +} } // namespace internal // Formatting context. @@ -1286,6 +1299,8 @@ inline format_arg_store make_format_args( return {args...}; } +template class dynamic_format_arg_store; + /** \rst A view of a collection of formatting arguments. To avoid lifetime issues it @@ -1357,6 +1372,17 @@ template class basic_format_args { set_data(store.data_); } + /** + \rst + Constructs a `dynamic_basic_format_args` object from `~fmt::format_arg_store`. + \endrst + */ + template + basic_format_args(const dynamic_format_arg_store& store) + : types_(store.get_types()) { + set_data(store.data_.data()); + } + /** \rst Constructs a `basic_format_args` object from a dynamic set of arguments. diff --git a/include/fmt/dyn-args.h b/include/fmt/dyn-args.h new file mode 100644 index 00000000..285b9bf5 --- /dev/null +++ b/include/fmt/dyn-args.h @@ -0,0 +1,199 @@ +// Copyright (c) 2020 Vladimir Solontsov +// SPDX-License-Identifier: MIT Licence + +#ifndef FMT_DYN_ARGS_H_ +#define FMT_DYN_ARGS_H_ + +#include +#include +#include + +#include "core.h" + +#if (defined(FMT_HAS_VARIANT) || __cplusplus >= 201703L) +# include +# ifndef FMT_HAS_VARIANT +# define FMT_HAS_VARIANT +# endif +#endif + +FMT_BEGIN_NAMESPACE + +namespace internal { + +template +struct is_string_view : std::false_type{}; + +template +struct is_string_view, Char> +: std::true_type{}; + +#ifdef FMT_USE_STRING_VIEW +template +struct is_string_view, Char> +: std::true_type{}; +#endif + +#ifdef FMT_USE_EXPERIMENTAL_STRING_VIEW +template +struct is_string_view, Char> +: std::true_type{}; +#endif + +template +struct is_ref_wrapper : std::false_type{}; + +template +struct is_ref_wrapper> : std::true_type{}; + +template +struct need_dyn_copy{ + using mapped_type = mapped_type_constant; + static_assert(mapped_type::value != internal::type::named_arg_type, + "Bug indicator. Named arguments must be processed separately"); + + using type = std::integral_constant::value || + is_string_view::value || + ( + mapped_type::value != internal::type::cstring_type && + mapped_type::value != internal::type::custom_type && + mapped_type::value != internal::type::string_type + ) + )>; +}; + +template using need_dyn_copy_t = + typename need_dyn_copy::type; + +template +const T& get(const StorageValue& v){ + return v; +} + +} // namespace internal + +/** + \rst + A dynamic version of `fmt::format_arg_store<>`. + It's equipped with a storage to potentially temporary objects which lifetime + could be shorter than the format arguments object. + + It can be implicitly converted into `~fmt::basic_format_args` for passing + into type-erased formatting functions such as `~fmt::vformat`. + \endrst + */ +template +class dynamic_format_arg_store +#if FMT_GCC_VERSION < 409 + // Workaround a GCC template argument substitution bug. + : public basic_format_args +#endif +{ + private: + using char_type = typename Context::char_type; + static const bool is_packed = false; + + static const bool has_custom_args = (sizeof...(Args) > 0); + using string_type = std::basic_string; +#ifdef FMT_HAS_VARIANT + using storage_item_type = + conditional_t, + string_type>; +#else + static_assert(!has_custom_args, "std::variant<> is required to support " + "custom types in dynamic_format_arg_store"); + using storage_item_type = string_type; +#endif + using value_type = basic_format_arg; + using named_value_type = internal::named_arg_base; + + template + using storaged_type = + conditional_t::value, string_type, T>; + + // Storage of basic_format_arg must be contiguous + // Required by basic_format_args::args_ which is just a pointer + std::vector data_; + + // Storage of arguments not fitting into basic_format_arg must grow + // without relocation because items in data_ refer to it. + + std::forward_list storage_; + + // Storage of serialized name_args. Must grow without relocation + // because items in data_ refer to it. + std::forward_list named_args_; + + friend class basic_format_args; + + template + const T& get_last_pushed() const{ + using internal::get; + return get(storage_.front()); + } + + unsigned long long get_types() const { + return internal::is_unpacked_bit | data_.size(); + } + + template const T& stored_value(const T& arg, std::false_type) { + return arg; + } + + template const T& stored_value( + const std::reference_wrapper& arg, std::false_type) { + return arg.get(); + } + + template const storaged_type& stored_value(const T& arg, + std::true_type) { + using type = storaged_type; + storage_.emplace_front(type{arg}); + return get_last_pushed(); + } + + template void emplace_arg(const T& arg) { + data_.emplace_back(internal::make_arg(arg)); + } + + public: + dynamic_format_arg_store() FMT_NOEXCEPT = default; + ~dynamic_format_arg_store() FMT_NOEXCEPT = default; + + dynamic_format_arg_store(const dynamic_format_arg_store&) = delete; + dynamic_format_arg_store& operator=(const dynamic_format_arg_store&) = delete; + + dynamic_format_arg_store(dynamic_format_arg_store&&) = default; + dynamic_format_arg_store& operator=(dynamic_format_arg_store&&) = default; + + template void push_back(const T& arg) { + emplace_arg(stored_value(arg, internal::need_dyn_copy_t{})); + } + + template void push_back(std::reference_wrapper arg) { + emplace_arg(arg.get()); + } + + template void push_back( + const internal::named_arg& arg) { + // Named argument is tricky. It's returned by value from fmt::arg() + // and then pointer to it is stored in basic_format_arg<>. + // So after end of expression the pointer becomes dangling. + storage_.emplace_front(string_type{arg.name.data(), arg.name.size()}); + basic_string_view name = get_last_pushed(); + const auto& val = stored_value( + arg.value, internal::need_dyn_copy_t{}); + + auto named_with_stored_parts = fmt::arg(name, val); + // Serialize value into base + internal::arg_mapper().map(named_with_stored_parts); + named_args_.push_front(named_with_stored_parts); + data_.emplace_back(internal::make_arg(named_args_.front())); + } +}; + +FMT_END_NAMESPACE + +#endif /* FMT_DYN_ARGS_H_ */ diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 89176633..23ea9fcf 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -95,6 +95,7 @@ add_fmt_test(grisu-test) target_compile_definitions(grisu-test PRIVATE FMT_USE_GRISU=1) add_fmt_test(gtest-extra-test) add_fmt_test(format-test mock-allocator.h) +add_fmt_test(format-dyn-args-test) if (MSVC) target_compile_options(format-test PRIVATE /bigobj) endif () diff --git a/test/format-dyn-args-test.cc b/test/format-dyn-args-test.cc new file mode 100644 index 00000000..1ef14d1a --- /dev/null +++ b/test/format-dyn-args-test.cc @@ -0,0 +1,124 @@ +// Copyright (c) 2020 Vladimir Solontsov +// SPDX-License-Identifier: MIT Licence + +#include + +#include "gtest-extra.h" + + +TEST(FormatDynArgsTest, Basic) { + fmt::dynamic_format_arg_store store; + store.push_back(42); + store.push_back("abc1"); + store.push_back(1.5f); + + std::string result = fmt::vformat( + "{} and {} and {}", + store); + + EXPECT_EQ("42 and abc1 and 1.5", result); +} + +TEST(FormatDynArgsTest, StringsAndRefs) { + // Unfortunately the tests are compiled with old ABI + // So strings use COW. + fmt::dynamic_format_arg_store store; + char str[]{"1234567890"}; + store.push_back(str); + store.push_back(std::cref(str)); + store.push_back(std::string_view{str}); + str[0] = 'X'; + + std::string result = fmt::vformat( + "{} and {} and {}", + store); + + EXPECT_EQ("1234567890 and X234567890 and X234567890", result); +} + +struct Custom{ int i{0}; }; +template <> +struct fmt::formatter { + constexpr auto parse(format_parse_context& ctx) { + return ctx.begin(); + } + + template + auto format(const Custom& p, FormatContext& ctx) { + // ctx.out() is an output iterator to write to. + return format_to( + ctx.out(), + "cust={}", p.i); + } +}; + +#ifdef FMT_HAS_VARIANT + +TEST(FormatDynArgsTest, CustomFormat) { + fmt::dynamic_format_arg_store store; + Custom c{}; + store.push_back(c); + ++c.i; + store.push_back(c); + ++c.i; + store.push_back(std::cref(c)); + ++c.i; + + std::string result = fmt::vformat( + "{} and {} and {}", + store); + + EXPECT_EQ("cust=0 and cust=1 and cust=3", result); +} + +#endif // FMT_HAS_VARIANT + +TEST(FormatDynArgsTest, NamedArgByRef) { + fmt::dynamic_format_arg_store store; + auto a1 = fmt::arg("a1_", 42); + store.push_back(std::cref(a1)); + + std::string result = fmt::vformat( + "{a1_}", // and {} and {}", + store); + + EXPECT_EQ("42", result); +} + +TEST(FormatDynArgsTest, NamedInt) { + fmt::dynamic_format_arg_store store; + store.push_back(fmt::arg("a1", 42)); + std::string result = fmt::vformat("{a1}", store); + EXPECT_EQ("42", result); +} + +TEST(FormatDynArgsTest, NamedStrings) { + fmt::dynamic_format_arg_store store; + char str[]{"1234567890"}; + store.push_back(fmt::arg("a1", str)); + store.push_back(fmt::arg("a2", std::cref(str))); + str[0] = 'X'; + + std::string result = fmt::vformat( + "{a1} and {a2}", + store); + + EXPECT_EQ("1234567890 and X234567890", result); +} + +TEST(FormatDynArgsTest, NamedCustomFormat) { + fmt::dynamic_format_arg_store store; + Custom c{}; + store.push_back(fmt::arg("a1", c)); + ++c.i; + store.push_back(fmt::arg("a2", c)); + ++c.i; + store.push_back(fmt::arg("a3", std::cref(c))); + ++c.i; + + std::string result = fmt::vformat( + "{a1} and {a2} and {a3}", + store); + + EXPECT_EQ("cust=0 and cust=1 and cust=3", result); +}