From 270a65e40d754d9a26e3539959db5c5d39b8a44b Mon Sep 17 00:00:00 2001 From: Baber Nawaz Date: Mon, 27 Jan 2025 22:47:51 +0000 Subject: [PATCH] chore: Replace use of ostream IO manipulators Currently, our streaming overloads (operator<<) make use of io manipulators. This means any ostreams making use of such overloads are also required to implement IO manipulators -- this adds non-trivial complexity and state. Not only that, they are cumbersome to use, and typically require saving existing state and restoring it with an RAII object -- this is often forgotten, as is the case in our codebase. The replacment is built on top of C++20's std::format. SDB-8506 --- bench/CMakeLists.txt | 6 ++++- bench/Stream.bm.cpp | 58 ++++++++++++++++++++++++++++++++++++++++ toolbox/hdr/Utility.hpp | 29 ++++++++------------ toolbox/sys/Log.ut.cpp | 15 ----------- toolbox/sys/Time.hpp | 13 +++------ toolbox/sys/Time.ut.cpp | 48 +++++++++++++++++++++++++++++++++ toolbox/util/Options.hpp | 3 ++- toolbox/util/Stream.hpp | 41 ++++++++++++++++++++++++++++ toolbox/util/Utility.hpp | 5 ++++ 9 files changed, 174 insertions(+), 44 deletions(-) create mode 100644 bench/Stream.bm.cpp diff --git a/bench/CMakeLists.txt b/bench/CMakeLists.txt index b96a24c0..6e03eba2 100644 --- a/bench/CMakeLists.txt +++ b/bench/CMakeLists.txt @@ -20,7 +20,8 @@ set(targets tb-map-bench tb-time-bench tb-timer-bench - tb-util-bench) + tb-util-bench + tb-stream-bench) add_custom_target(tb-bench DEPENDS ${targets}) @@ -44,3 +45,6 @@ target_link_libraries(tb-timer-bench ${tb_bm_LIBRARY}) add_executable(tb-util-bench Util.bm.cpp) target_link_libraries(tb-util-bench ${tb_bm_LIBRARY}) + +add_executable(tb-stream-bench Stream.bm.cpp) +target_link_libraries(tb-stream-bench ${tb_bm_LIBRARY}) diff --git a/bench/Stream.bm.cpp b/bench/Stream.bm.cpp new file mode 100644 index 00000000..686f58d0 --- /dev/null +++ b/bench/Stream.bm.cpp @@ -0,0 +1,58 @@ +// The Reactive C++ Toolbox. +// Copyright (C) 2025 Reactive Markets Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#include +#include + +TOOLBOX_BENCHMARK_MAIN + +using namespace std; +using namespace toolbox; + +namespace { + +TOOLBOX_BENCHMARK(std_io_manipulator) +{ + stringstream ss; + while (ctx) { + ss.str(string{}); // reset it + for ([[maybe_unused]] auto i : ctx.range(100)) { + ss << setw(20) << fixed << setprecision(6) << double{1.2345678} << '\n'; + ss << setw(10) << setfill('0') << 1234 << '\n'; + ss << setw(10) << setfill('*') << 54 << '\n'; + bm::do_not_optimise(ss); + } + } +} + +TOOLBOX_BENCHMARK(std_format_manipulator) +{ + stringstream ss; + while (ctx) { + ss.str(string{}); // reset it + for ([[maybe_unused]] auto i : ctx.range(100)) { + ss << std::format("{:20.6f}", double{1.2345678}) << '\n'; + ss << std::format("{:0>10}", int{1234}) << '\n'; + ss << std::format("{:*>10}", 54) << '\n'; + bm::do_not_optimise(ss); + } + } +} + +} // namespace diff --git a/toolbox/hdr/Utility.hpp b/toolbox/hdr/Utility.hpp index 1e02ec50..6d395e4f 100644 --- a/toolbox/hdr/Utility.hpp +++ b/toolbox/hdr/Utility.hpp @@ -19,6 +19,7 @@ #include "Histogram.hpp" #include +#include #include #include @@ -69,8 +70,6 @@ template requires Streamable StreamT& operator<<(StreamT& os, PutPercentiles pp) { - using namespace std; - const auto sf = pp.h.significant_figures(); boost::io::ios_all_saver all_saver{os}; @@ -82,15 +81,13 @@ StreamT& operator<<(StreamT& os, PutPercentiles pp) const double percentile{iter.percentile() / 100.0}; const int64_t total_count{iter.cumulative_count()}; - // clang-format off - os << setw(12) << fixed << setprecision(sf) << value - << setw(15) << fixed << setprecision(6) << percentile - << setw(11) << total_count; - // clang-format on + os << std::format("{:12.{}f}", value, sf); + os << std::format("{:15.6f}", percentile); + os << std::format("{:11}", total_count); if (percentile < 1.0) { const double inverted_percentile{(1.0 / (1.0 - percentile))}; - os << setw(15) << fixed << setprecision(2) << inverted_percentile; + os << std::format("{:15.2f}", inverted_percentile); } os << '\n'; } @@ -101,18 +98,14 @@ StreamT& operator<<(StreamT& os, PutPercentiles pp) const int64_t total_val{pp.h.total_count()}; // clang-format off - os << "#[Mean = " << setw(12) << fixed << setprecision(sf) << mean_val - << ", StdDeviation = " << setw(12) << fixed << setprecision(sf) << stddev_val - << "]\n" - "#[Max = " << setw(12) << fixed << setprecision(sf) << max_val - << ", TotalCount = " << setw(12) << total_val - << "]\n" - "#[Buckets = " << setw(12) << pp.h.bucket_count() - << ", SubBuckets = " << setw(12) << pp.h.sub_bucket_count() + os << "#[Mean = " << std::format("{:12.{}f}", mean_val, sf) + << ", StdDeviation = " << std::format("{:12.{}f}", stddev_val, sf) + << "]\n#[Max = " << std::format("{:12.{}f}", max_val, sf) + << ", TotalCount = " << std::format("{:12}", total_val) + << "]\n#[Buckets = " << std::format("{:12}", pp.h.bucket_count()) + << ", SubBuckets = " << std::format("{:12}", pp.h.sub_bucket_count()) << "]"; - return os; - // clang-format on } diff --git a/toolbox/sys/Log.ut.cpp b/toolbox/sys/Log.ut.cpp index a77f5a1c..792ad2d0 100644 --- a/toolbox/sys/Log.ut.cpp +++ b/toolbox/sys/Log.ut.cpp @@ -28,14 +28,6 @@ using namespace std; using namespace toolbox; namespace { -namespace noformat { -// Specific Log operator<< to allow non formatted writing. -Log& operator<<(Log& log, std::string_view str) -{ - return log(str.data(), str.size()); -} -} // namespace noformat - template struct Foo { T first; @@ -126,13 +118,6 @@ BOOST_AUTO_TEST_CASE(LogMacroCase) TOOLBOX_DEBUG << "test8: " << Foo{10, 20}; BOOST_CHECK_EQUAL(tl.last_level, LogLevel::Info); BOOST_CHECK_EQUAL(tl.last_msg, "test7: (10,20)"); - - // This will log a non formatted string view, the formatting shows up on the next "formatable" - // parameter. - using namespace noformat; - TOOLBOX_LOG(LogLevel::Info) << setw(3) << setfill('*') << "test8: "sv << Foo{10, 20}; - BOOST_CHECK_EQUAL(tl.last_level, LogLevel::Info); - BOOST_CHECK_EQUAL(tl.last_msg, "test8: **(10,20)"); } BOOST_AUTO_TEST_SUITE_END() diff --git a/toolbox/sys/Time.hpp b/toolbox/sys/Time.hpp index d9b74eed..130753eb 100644 --- a/toolbox/sys/Time.hpp +++ b/toolbox/sys/Time.hpp @@ -19,6 +19,7 @@ #include #include +#include #include @@ -281,19 +282,13 @@ StreamT& operator<<(StreamT& os, PutTime pt) if constexpr (std::is_same_v) { const auto ns = ns_since_epoch(pt.time); - boost::io::ios_fill_saver ifs{os}; - boost::io::ios_width_saver iws{os}; - os << '.' << std::setfill('0') << std::setw(9) << (ns % 1'000'000'000L); + os << '.' << std::format("{:0>9}", ns % 1'000'000'000L); } else if constexpr (std::is_same_v) { const auto us = us_since_epoch(pt.time); - boost::io::ios_fill_saver ifs{os}; - boost::io::ios_width_saver iws{os}; - os << '.' << std::setfill('0') << std::setw(6) << (us % 1'000'000L); + os << '.' << std::format("{:0>6}", us % 1'000'000L); } else if constexpr (std::is_same_v) { const auto ms = ms_since_epoch(pt.time); - boost::io::ios_fill_saver ifs{os}; - boost::io::ios_width_saver iws{os}; - os << '.' << std::setfill('0') << std::setw(3) << (ms % 1'000L); + os << '.' << std::format("{:0>3}", ms % 1'000L); } else if constexpr (std::is_same_v) { } else { static_assert(AlwaysFalse::value); diff --git a/toolbox/sys/Time.ut.cpp b/toolbox/sys/Time.ut.cpp index 279750a0..72d6fb09 100644 --- a/toolbox/sys/Time.ut.cpp +++ b/toolbox/sys/Time.ut.cpp @@ -15,8 +15,10 @@ // limitations under the License. #include "Time.hpp" +#include "Date.hpp" #include +#include namespace std::chrono { template @@ -76,4 +78,50 @@ BOOST_AUTO_TEST_CASE(TimeParseTimeOnlyCase) BOOST_CHECK_EQUAL(*parse_time_only("00:00:00.000000789"sv), 789ns); } +BOOST_AUTO_TEST_CASE(PutTimeOutput) +{ + std::stringstream stream; + + auto tm = parse_time("20180824T05:32:29.123456789"sv); + BOOST_CHECK(tm.has_value()); + + stream << put_time(*tm, "%Y%m%dT%H:%M:%S"); + BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29"); + + stream.str(""); + stream << put_time(*tm, "%Y%m%dT%H:%M:%S"); + BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29.123"); + + stream.str(""); + stream << put_time(*tm, "%Y%m%dT%H:%M:%S"); + BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29.123456"); + + stream.str(""); + stream << put_time(*tm, "%Y%m%dT%H:%M:%S"); + BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29.123456789"); +} + +BOOST_AUTO_TEST_CASE(PutTimeOutput2) +{ + std::stringstream stream; + + auto tm = parse_time("20180824T05:32:29.001001001"sv); + BOOST_CHECK(tm.has_value()); + + stream << put_time(*tm, "%Y%m%dT%H:%M:%S"); + BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29"); + + stream.str(""); + stream << put_time(*tm, "%Y%m%dT%H:%M:%S"); + BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29.001"); + + stream.str(""); + stream << put_time(*tm, "%Y%m%dT%H:%M:%S"); + BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29.001001"); + + stream.str(""); + stream << put_time(*tm, "%Y%m%dT%H:%M:%S"); + BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29.001001001"); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/toolbox/util/Options.hpp b/toolbox/util/Options.hpp index 9f23f840..4d3c909a 100644 --- a/toolbox/util/Options.hpp +++ b/toolbox/util/Options.hpp @@ -26,6 +26,7 @@ /// \author Rodrigo Fernandes #include +#include #include #include @@ -245,7 +246,7 @@ StreamT& operator<<(StreamT& out, const Options& options) max_width -= 2 + opt->long_opt.size(); out << "--" << opt->long_opt; } - out << std::setw(max_width) << ' ' << opt->description << "\n"; + out << std::format("{:{}}", ' ', max_width) << opt->description << '\n'; } return out; } diff --git a/toolbox/util/Stream.hpp b/toolbox/util/Stream.hpp index 13ee3ba7..15d9dcc9 100644 --- a/toolbox/util/Stream.hpp +++ b/toolbox/util/Stream.hpp @@ -17,10 +17,14 @@ #ifndef TOOLBOX_UTIL_STREAM_HPP #define TOOLBOX_UTIL_STREAM_HPP +#include #include #include +#include +#include + namespace toolbox { inline namespace util { namespace detail { @@ -205,6 +209,43 @@ class OStaticStream final : public std::ostream { StaticStreamBuf buf_; }; +// Similar to std::ostream_iterator, but this works with any "Streamable" type. +template + requires Streamable +class OStreamIterator { + public: + OStreamIterator() = delete; + + explicit OStreamIterator(StreamT& os, const char* delim = nullptr) noexcept + : os_(&os) + , delim_(delim) + { + } + + template + OStreamIterator& operator=(const T& value) + { + *os_ << value; + if (delim_) [[unlikely]] { + *os_ << delim_; + } + return *this; + } + + OStreamIterator& operator*() noexcept { return *this; } + OStreamIterator& operator++() noexcept { return *this; } + OStreamIterator& operator++(int) noexcept { return *this; } + + // required by std::output_iterator concept + using difference_type = ptrdiff_t; + + private: + // Pointer (instead of reference) because this class needs to be assignable + // to satisfy std::output_iterator concept (references can't be assigned). + StreamT* os_; + const char* delim_{nullptr}; +}; + using OStreamJoiner = std::experimental::ostream_joiner; template diff --git a/toolbox/util/Utility.hpp b/toolbox/util/Utility.hpp index dbdd772c..39227f76 100644 --- a/toolbox/util/Utility.hpp +++ b/toolbox/util/Utility.hpp @@ -165,6 +165,11 @@ struct string_hash { } }; +inline constexpr std::string_view bool_to_alpha(bool b) noexcept +{ + return b ? "true" : "false"; +} + } // namespace util } // namespace toolbox