Skip to content

Commit

Permalink
chore: Replace use of ostream IO manipulators
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bnbajwa committed Feb 12, 2025
1 parent f091245 commit 7f831b1
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 54 deletions.
6 changes: 5 additions & 1 deletion bench/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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})

Expand All @@ -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})
58 changes: 58 additions & 0 deletions bench/Stream.bm.cpp
Original file line number Diff line number Diff line change
@@ -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 <toolbox/bm.hpp>
#include <toolbox/bm/Utility.hpp>
#include <toolbox/util/Stream.hpp>

#include <sstream>
#include <iomanip>

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
33 changes: 11 additions & 22 deletions toolbox/hdr/Utility.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,8 @@
#include <toolbox/util/Concepts.hpp>
#include <toolbox/Config.h>

#include <boost/io/ios_state.hpp>

#include <cstdint>
#include <iomanip>
#include <format>

namespace toolbox {
/// A C++ port of HdrHistogram_c written Michael Barker and released to the public domain.
Expand Down Expand Up @@ -69,10 +67,7 @@ template <typename StreamT>
requires Streamable<StreamT>
StreamT& operator<<(StreamT& os, PutPercentiles pp)
{
using namespace std;

const auto sf = pp.h.significant_figures();
boost::io::ios_all_saver all_saver{os};

os << " Value Percentile TotalCount 1/(1-Percentile)\n\n";

Expand All @@ -82,15 +77,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';
}
Expand All @@ -101,18 +94,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
}


Expand Down
17 changes: 0 additions & 17 deletions toolbox/sys/Log.ut.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,11 @@
#include <boost/test/unit_test.hpp>

#include <cstring>
#include <iomanip>
#include <string_view>

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 <typename T, typename U>
struct Foo {
T first;
Expand Down Expand Up @@ -126,13 +116,6 @@ BOOST_AUTO_TEST_CASE(LogMacroCase)
TOOLBOX_DEBUG << "test8: " << Foo<int, int>{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<int, int>{10, 20};
BOOST_CHECK_EQUAL(tl.last_level, LogLevel::Info);
BOOST_CHECK_EQUAL(tl.last_msg, "test8: **(10,20)");
}

BOOST_AUTO_TEST_SUITE_END()
17 changes: 5 additions & 12 deletions toolbox/sys/Time.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@

#include <toolbox/util/TypeTraits.hpp>
#include <toolbox/util/Concepts.hpp>

#include <boost/io/ios_state.hpp>
#include <toolbox/util/Stream.hpp>

#include <chrono>
#include <iomanip>
#include <format>
#include <optional>

namespace toolbox {
Expand Down Expand Up @@ -281,19 +280,13 @@ StreamT& operator<<(StreamT& os, PutTime<DurationT> pt)

if constexpr (std::is_same_v<DurationT, Nanos>) {
const auto ns = ns_since_epoch<WallClock>(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<DurationT, Micros>) {
const auto us = us_since_epoch<WallClock>(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<DurationT, Millis>) {
const auto ms = ms_since_epoch<WallClock>(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<DurationT, Seconds>) {
} else {
static_assert(AlwaysFalse<DurationT>::value);
Expand Down
48 changes: 48 additions & 0 deletions toolbox/sys/Time.ut.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
// limitations under the License.

#include "Time.hpp"
#include "Date.hpp"

#include <boost/test/unit_test.hpp>
#include <sstream>

namespace std::chrono {
template <typename RepT, typename PeriodT>
Expand Down Expand Up @@ -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<Seconds>(*tm, "%Y%m%dT%H:%M:%S");
BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29");

stream.str("");
stream << put_time<Millis>(*tm, "%Y%m%dT%H:%M:%S");
BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29.123");

stream.str("");
stream << put_time<Micros>(*tm, "%Y%m%dT%H:%M:%S");
BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29.123456");

stream.str("");
stream << put_time<Nanos>(*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<Seconds>(*tm, "%Y%m%dT%H:%M:%S");
BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29");

stream.str("");
stream << put_time<Millis>(*tm, "%Y%m%dT%H:%M:%S");
BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29.001");

stream.str("");
stream << put_time<Micros>(*tm, "%Y%m%dT%H:%M:%S");
BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29.001001");

stream.str("");
stream << put_time<Nanos>(*tm, "%Y%m%dT%H:%M:%S");
BOOST_CHECK_EQUAL(stream.str(), "20180824T05:32:29.001001001");
}

BOOST_AUTO_TEST_SUITE_END()
5 changes: 3 additions & 2 deletions toolbox/util/Options.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@
/// \author Rodrigo Fernandes

#include <toolbox/util/RefCount.hpp>
#include <toolbox/util/Stream.hpp>
#include <toolbox/util/String.hpp>

#include <map>
#include <variant>
#include <iomanip>
#include <format>

namespace toolbox {
inline namespace util {
Expand Down Expand Up @@ -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;
}
Expand Down
38 changes: 38 additions & 0 deletions toolbox/util/Stream.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#ifndef TOOLBOX_UTIL_STREAM_HPP
#define TOOLBOX_UTIL_STREAM_HPP

#include <toolbox/util/Concepts.hpp>
#include <toolbox/util/Storage.hpp>

#include <experimental/iterator>
Expand Down Expand Up @@ -205,6 +206,43 @@ class OStaticStream final : public std::ostream {
StaticStreamBuf<MaxN> buf_;
};

// Similar to std::ostream_iterator, but this works with any "Streamable" type.
template <class StreamT>
requires Streamable<StreamT>
class OStreamIterator {
public:
OStreamIterator() = delete;

explicit OStreamIterator(StreamT& os, const char* delim = nullptr) noexcept
: os_(&os)
, delim_(delim)
{
}

template <class T>
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<char>;

template <auto DelimT, typename ArgT, typename... ArgsT>
Expand Down
5 changes: 5 additions & 0 deletions toolbox/util/Utility.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 7f831b1

Please # to comment.