From f3a2b5237688e9f574444e793724664b00fb7f2a Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Wed, 22 Nov 2023 14:47:00 -0500 Subject: [PATCH 1/7] serialization: Support for multiple parameters This commit makes a minimal change to the ParamsStream class to let it retrieve multiple parameters. Followup commits after this commit clean up code using ParamsStream and make it easier to set multiple parameters. Currently it is only possible to attach one serialization parameter to a stream at a time. For example, it is not possible to set a parameter controlling the transaction format and a parameter controlling the address format at the same time because one parameter will override the other. This limitation is inconvenient for multiprocess code since it is not possible to create just one type of stream and serialize any object to it. Instead it is necessary to create different streams for different object types, which requires extra boilerplate and makes using the new parameter fields a lot more awkward than the older version and type fields. Fix this problem by allowing an unlimited number of serialization stream parameters to be set, and allowing them to be requested by type. Later parameters will still override earlier parameters, but only if they have the same type. This change requires replacing the stream.GetParams() method with a stream.GetParams() method in order for serialization code to retrieve the desired parameters. This change is more verbose, but probably a good thing for readability because previously it could be difficult to know what type the GetParams() method would return, and now it is more obvious. --- src/netaddress.h | 4 ++-- src/primitives/transaction.h | 6 +++--- src/protocol.h | 3 ++- src/serialize.h | 39 ++++++++++++++++-------------------- src/test/serialize_tests.cpp | 7 ++++--- 5 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/netaddress.h b/src/netaddress.h index 08dd77c0ffa..5e5c412586f 100644 --- a/src/netaddress.h +++ b/src/netaddress.h @@ -241,7 +241,7 @@ public: template void Serialize(Stream& s) const { - if (s.GetParams().enc == Encoding::V2) { + if (s.template GetParams().enc == Encoding::V2) { SerializeV2Stream(s); } else { SerializeV1Stream(s); @@ -254,7 +254,7 @@ public: template void Unserialize(Stream& s) { - if (s.GetParams().enc == Encoding::V2) { + if (s.template GetParams().enc == Encoding::V2) { UnserializeV2Stream(s); } else { UnserializeV1Stream(s); diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index ccbeb3ec49b..d15b8005f90 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -326,7 +326,7 @@ public: template inline void Serialize(Stream& s) const { - SerializeTransaction(*this, s, s.GetParams()); + SerializeTransaction(*this, s, s.template GetParams()); } /** This deserializing constructor is provided instead of an Unserialize method. @@ -386,12 +386,12 @@ struct CMutableTransaction template inline void Serialize(Stream& s) const { - SerializeTransaction(*this, s, s.GetParams()); + SerializeTransaction(*this, s, s.template GetParams()); } template inline void Unserialize(Stream& s) { - UnserializeTransaction(*this, s, s.GetParams()); + UnserializeTransaction(*this, s, s.template GetParams()); } template diff --git a/src/protocol.h b/src/protocol.h index e405253632d..58a7287d036 100644 --- a/src/protocol.h +++ b/src/protocol.h @@ -406,9 +406,10 @@ public: static constexpr SerParams V1_DISK{{CNetAddr::Encoding::V1}, Format::Disk}; static constexpr SerParams V2_DISK{{CNetAddr::Encoding::V2}, Format::Disk}; - SERIALIZE_METHODS_PARAMS(CAddress, obj, SerParams, params) + SERIALIZE_METHODS(CAddress, obj) { bool use_v2; + auto& params = SER_PARAMS(SerParams); if (params.fmt == Format::Disk) { // In the disk serialization format, the encoding (v1 or v2) is determined by a flag version // that's part of the serialization itself. ADDRV2_FORMAT in the stream version only determines diff --git a/src/serialize.h b/src/serialize.h index 19585c630ab..44e2db7ac7b 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -181,9 +181,8 @@ const Out& AsBase(const In& x) static void SerializationOps(Type& obj, Stream& s, Operation ser_action) /** - * Variant of FORMATTER_METHODS that supports a declared parameter type. - * - * If a formatter has a declared parameter type, it must be invoked directly or + * Formatter methods can retrieve parameters attached to a stream using the + * SER_PARAMS(type) macro as long as the stream is created directly or * indirectly with a parameter of that type. This permits making serialization * depend on run-time context in a type-safe way. * @@ -191,7 +190,8 @@ const Out& AsBase(const In& x) * struct BarParameter { bool fancy; ... }; * struct Bar { ... }; * struct FooFormatter { - * FORMATTER_METHODS(Bar, obj, BarParameter, param) { + * FORMATTER_METHODS(Bar, obj) { + * auto& param = SER_PARAMS(BarParameter); * if (param.fancy) { * READWRITE(VARINT(obj.value)); * } else { @@ -213,13 +213,7 @@ const Out& AsBase(const In& x) * Compilation will fail in any context where serialization is invoked but * no parameter of a type convertible to BarParameter is provided. */ -#define FORMATTER_METHODS_PARAMS(cls, obj, paramcls, paramobj) \ - template \ - static void Ser(Stream& s, const cls& obj) { SerializationOps(obj, s, ActionSerialize{}, s.GetParams()); } \ - template \ - static void Unser(Stream& s, cls& obj) { SerializationOps(obj, s, ActionUnserialize{}, s.GetParams()); } \ - template \ - static void SerializationOps(Type& obj, Stream& s, Operation ser_action, const paramcls& paramobj) +#define SER_PARAMS(type) (s.template GetParams()) #define BASE_SERIALIZE_METHODS(cls) \ template \ @@ -246,15 +240,6 @@ const Out& AsBase(const In& x) BASE_SERIALIZE_METHODS(cls) \ FORMATTER_METHODS(cls, obj) -/** - * Variant of SERIALIZE_METHODS that supports a declared parameter type. - * - * See FORMATTER_METHODS_PARAMS for more information on parameters. - */ -#define SERIALIZE_METHODS_PARAMS(cls, obj, paramcls, paramobj) \ - BASE_SERIALIZE_METHODS(cls) \ - FORMATTER_METHODS_PARAMS(cls, obj, paramcls, paramobj) - // Templates for serializing to anything that looks like a stream, // i.e. anything that supports .read(Span) and .write(Span) // @@ -1134,9 +1119,19 @@ public: void ignore(size_t num) { m_substream.ignore(num); } bool eof() const { return m_substream.eof(); } size_t size() const { return m_substream.size(); } - const Params& GetParams() const { return m_params; } int GetVersion() = delete; // Deprecated with Params usage int GetType() = delete; // Deprecated with Params usage + + //! Get reference to stream parameters. + template + const auto& GetParams() const + { + if constexpr (std::is_convertible_v) { + return m_params; + } else { + return m_substream.template GetParams

(); + } + } }; /** Wrapper that serializes objects with the specified parameters. */ @@ -1176,7 +1171,7 @@ public: /** \ * Return a wrapper around t that (de)serializes it with specified parameter params. \ * \ - * See FORMATTER_METHODS_PARAMS for more information on serialization parameters. \ + * See SER_PARAMS for more information on serialization parameters. \ */ \ template \ auto operator()(T&& t) const \ diff --git a/src/test/serialize_tests.cpp b/src/test/serialize_tests.cpp index d75eb499b4a..18356f2df03 100644 --- a/src/test/serialize_tests.cpp +++ b/src/test/serialize_tests.cpp @@ -289,7 +289,7 @@ public: template void Serialize(Stream& s) const { - if (s.GetParams().m_base_format == BaseFormat::RAW) { + if (s.template GetParams().m_base_format == BaseFormat::RAW) { s << m_base_data; } else { s << Span{HexStr(Span{&m_base_data, 1})}; @@ -299,7 +299,7 @@ public: template void Unserialize(Stream& s) { - if (s.GetParams().m_base_format == BaseFormat::RAW) { + if (s.template GetParams().m_base_format == BaseFormat::RAW) { s >> m_base_data; } else { std::string hex{"aa"}; @@ -327,8 +327,9 @@ class Derived : public Base public: std::string m_derived_data; - SERIALIZE_METHODS_PARAMS(Derived, obj, DerivedAndBaseFormat, fmt) + SERIALIZE_METHODS(Derived, obj) { + auto& fmt = SER_PARAMS(DerivedAndBaseFormat); READWRITE(fmt.m_base_format(AsBase(obj))); if (ser_action.ForRead()) { From 84502b755bcc35413ad466047893b5edf134c53f Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Wed, 21 Feb 2024 07:07:50 -0500 Subject: [PATCH 2/7] serialization: Drop references to GetVersion/GetType Drop references to stream GetVersion()/GetType() methods which no longer exist --- src/serialize.h | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index 44e2db7ac7b..08d9a80f967 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -1103,12 +1103,12 @@ size_t GetSerializeSize(const T& t) return (SizeComputer() << t).size(); } -/** Wrapper that overrides the GetParams() function of a stream (and hides GetVersion/GetType). */ +/** Wrapper that overrides the GetParams() function of a stream. */ template class ParamsStream { const Params& m_params; - SubStream& m_substream; // private to avoid leaking version/type into serialization code that shouldn't see it + SubStream& m_substream; public: ParamsStream(const Params& params LIFETIMEBOUND, SubStream& substream LIFETIMEBOUND) : m_params{params}, m_substream{substream} {} @@ -1119,8 +1119,6 @@ public: void ignore(size_t num) { m_substream.ignore(num); } bool eof() const { return m_substream.eof(); } size_t size() const { return m_substream.size(); } - int GetVersion() = delete; // Deprecated with Params usage - int GetType() = delete; // Deprecated with Params usage //! Get reference to stream parameters. template From 83436d14f06551026bcf5529df3b63b4e8a679fb Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Wed, 22 Nov 2023 16:39:32 -0500 Subject: [PATCH 3/7] serialization: Drop unnecessary ParamsStream references Drop unnecessary ParamsStream references from CTransaction and CMutableTransaction constructors. This just couples these classes unnecessarily to the ParamsStream class, making the ParamsStream class harder to modify, and making the transaction classes in some cases (depending on parameter order) unable to work with stream classes that have multiple parameters set. --- src/primitives/transaction.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index d15b8005f90..976542cfae6 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -334,7 +334,7 @@ public: template CTransaction(deserialize_type, const TransactionSerParams& params, Stream& s) : CTransaction(CMutableTransaction(deserialize, params, s)) {} template - CTransaction(deserialize_type, ParamsStream& s) : CTransaction(CMutableTransaction(deserialize, s)) {} + CTransaction(deserialize_type, Stream& s) : CTransaction(CMutableTransaction(deserialize, s)) {} bool IsNull() const { return vin.empty() && vout.empty(); @@ -400,7 +400,7 @@ struct CMutableTransaction } template - CMutableTransaction(deserialize_type, ParamsStream& s) { + CMutableTransaction(deserialize_type, Stream& s) { Unserialize(s); } From cb28849a88339c1e7ba03ffc7e38998339074e6e Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Wed, 22 Nov 2023 16:39:32 -0500 Subject: [PATCH 4/7] serialization: Reverse ParamsStream constructor order Move parameter argument after stream argument so will be possible to accept multiple variadic parameter arguments in the following commit. Also reverse template parameter order for consistency. --- src/addrman.cpp | 4 ++-- src/net.cpp | 2 +- src/serialize.h | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/addrman.cpp b/src/addrman.cpp index a8206de6ee2..b649950eced 100644 --- a/src/addrman.cpp +++ b/src/addrman.cpp @@ -171,7 +171,7 @@ void AddrManImpl::Serialize(Stream& s_) const */ // Always serialize in the latest version (FILE_FORMAT). - ParamsStream s{CAddress::V2_DISK, s_}; + ParamsStream s{s_, CAddress::V2_DISK}; s << static_cast(FILE_FORMAT); @@ -236,7 +236,7 @@ void AddrManImpl::Unserialize(Stream& s_) s_ >> Using>(format); const auto ser_params = (format >= Format::V3_BIP155 ? CAddress::V2_DISK : CAddress::V1_DISK); - ParamsStream s{ser_params, s_}; + ParamsStream s{s_, ser_params}; uint8_t compat; s >> compat; diff --git a/src/net.cpp b/src/net.cpp index 0cc794c2c3f..ba764e28e19 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -199,7 +199,7 @@ static std::vector ConvertSeeds(const std::vector &vSeedsIn) std::vector vSeedsOut; FastRandomContext rng; DataStream underlying_stream{vSeedsIn}; - ParamsStream s{CAddress::V2_NETWORK, underlying_stream}; + ParamsStream s{underlying_stream, CAddress::V2_NETWORK}; while (!s.eof()) { CService endpoint; s >> endpoint; diff --git a/src/serialize.h b/src/serialize.h index 08d9a80f967..8ad5ad5147b 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -1104,14 +1104,14 @@ size_t GetSerializeSize(const T& t) } /** Wrapper that overrides the GetParams() function of a stream. */ -template +template class ParamsStream { const Params& m_params; SubStream& m_substream; public: - ParamsStream(const Params& params LIFETIMEBOUND, SubStream& substream LIFETIMEBOUND) : m_params{params}, m_substream{substream} {} + ParamsStream(SubStream& substream LIFETIMEBOUND, const Params& params LIFETIMEBOUND) : m_params{params}, m_substream{substream} {} template ParamsStream& operator<<(const U& obj) { ::Serialize(*this, obj); return *this; } template ParamsStream& operator>>(U&& obj) { ::Unserialize(*this, obj); return *this; } void write(Span src) { m_substream.write(src); } @@ -1145,13 +1145,13 @@ public: template void Serialize(Stream& s) const { - ParamsStream ss{m_params, s}; + ParamsStream ss{s, m_params}; ::Serialize(ss, m_object); } template void Unserialize(Stream& s) { - ParamsStream ss{m_params, s}; + ParamsStream ss{s, m_params}; ::Unserialize(ss, m_object); } }; From e6794e475c84d9edca4a2876e2342cbb1d85f804 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Wed, 22 Nov 2023 17:56:04 -0500 Subject: [PATCH 5/7] serialization: Accept multiple parameters in ParamsStream constructor Before this change it was possible but awkward to create ParamStream streams with multiple parameter objects. After this change it is straightforward. The change to support multiple parameters is implemented by letting ParamsStream contain substream instances, instead of just references to external substreams. So a side-effect of this change is that ParamStream can now accept rvalue stream arguments and be easier to use in some other cases. A test for rvalues is added in this commit, and some simplifications to non-test code are made in the next commit. --- src/serialize.h | 32 ++++++++++++++- src/test/serialize_tests.cpp | 79 ++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index 8ad5ad5147b..8bca4d8ada1 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -1108,10 +1108,22 @@ template class ParamsStream { const Params& m_params; - SubStream& m_substream; + // If ParamsStream constructor is passed an lvalue argument, Substream will + // be a reference type, and m_substream will reference that argument. + // Otherwise m_substream will be a substream instance and move from the + // argument. Letting ParamsStream contain a substream instance instead of + // just a reference is useful to make the ParamsStream object self contained + // and let it do cleanup when destroyed, for example by closing files if + // SubStream is a file stream. + SubStream m_substream; public: - ParamsStream(SubStream& substream LIFETIMEBOUND, const Params& params LIFETIMEBOUND) : m_params{params}, m_substream{substream} {} + ParamsStream(SubStream&& substream, const Params& params LIFETIMEBOUND) : m_params{params}, m_substream{std::forward(substream)} {} + + template + ParamsStream(NestedSubstream&& s, const Params1& params1 LIFETIMEBOUND, const Params2& params2 LIFETIMEBOUND, const NestedParams&... params LIFETIMEBOUND) + : ParamsStream{::ParamsStream{std::forward(s), params2, params...}, params1} {} + template ParamsStream& operator<<(const U& obj) { ::Serialize(*this, obj); return *this; } template ParamsStream& operator>>(U&& obj) { ::Unserialize(*this, obj); return *this; } void write(Span src) { m_substream.write(src); } @@ -1132,6 +1144,22 @@ public: } }; +/** + * Explicit template deduction guide is required for single-parameter + * constructor so Substream&& is treated as a forwarding reference, and + * SubStream is deduced as reference type for lvalue arguments. + */ +template +ParamsStream(Substream&&, const Params&) -> ParamsStream; + +/** + * Template deduction guide for multiple params arguments that creates a nested + * ParamsStream. + */ +template +ParamsStream(Substream&& s, const Params1& params1, const Params2& params2, const Params&... params) -> + ParamsStream(s), params2, params...}), Params1>; + /** Wrapper that serializes objects with the specified parameters. */ template class ParamsWrapper diff --git a/src/test/serialize_tests.cpp b/src/test/serialize_tests.cpp index 18356f2df03..ad4852d13d2 100644 --- a/src/test/serialize_tests.cpp +++ b/src/test/serialize_tests.cpp @@ -15,6 +15,18 @@ BOOST_FIXTURE_TEST_SUITE(serialize_tests, BasicTestingSetup) +// For testing move-semantics, declare a version of datastream that can be moved +// but is not copyable. +class UncopyableStream : public DataStream +{ +public: + using DataStream::DataStream; + UncopyableStream(const UncopyableStream&) = delete; + UncopyableStream& operator=(const UncopyableStream&) = delete; + UncopyableStream(UncopyableStream&&) noexcept = default; + UncopyableStream& operator=(UncopyableStream&&) noexcept = default; +}; + class CSerializeMethodsTestSingle { protected: @@ -344,6 +356,73 @@ public: } }; +struct OtherParam { + uint8_t param; + SER_PARAMS_OPFUNC +}; + +//! Checker for value of OtherParam. When being serialized, serializes the +//! param to the stream. When being unserialized, verifies the value in the +//! stream matches the param. +class OtherParamChecker +{ +public: + template + void Serialize(Stream& s) const + { + const uint8_t param = s.template GetParams().param; + s << param; + } + + template + void Unserialize(Stream& s) const + { + const uint8_t param = s.template GetParams().param; + uint8_t value; + s >> value; + BOOST_CHECK_EQUAL(value, param); + } +}; + +//! Test creating a stream with multiple parameters and making sure +//! serialization code requiring different parameters can retrieve them. Also +//! test that earlier parameters take precedence if the same parameter type is +//! specified twice. (Choice of whether earlier or later values take precedence +//! or multiple values of the same type are allowed was arbitrary, and just +//! decided based on what would require smallest amount of ugly C++ template +//! code. Intent of the test is to just ensure there is no unexpected behavior.) +BOOST_AUTO_TEST_CASE(with_params_multi) +{ + const OtherParam other_param_used{.param = 0x10}; + const OtherParam other_param_ignored{.param = 0x11}; + const OtherParam other_param_override{.param = 0x12}; + const OtherParamChecker check; + DataStream stream; + ParamsStream pstream{stream, RAW, other_param_used, other_param_ignored}; + + Base base1{0x20}; + pstream << base1 << check << other_param_override(check); + BOOST_CHECK_EQUAL(stream.str(), "\x20\x10\x12"); + + Base base2; + pstream >> base2 >> check >> other_param_override(check); + BOOST_CHECK_EQUAL(base2.m_base_data, 0x20); +} + +//! Test creating a ParamsStream that moves from a stream argument. +BOOST_AUTO_TEST_CASE(with_params_move) +{ + UncopyableStream stream{}; + ParamsStream pstream{std::move(stream), RAW, HEX, RAW}; + + Base base1{0x20}; + pstream << base1; + + Base base2; + pstream >> base2; + BOOST_CHECK_EQUAL(base2.m_base_data, 0x20); +} + BOOST_AUTO_TEST_CASE(with_params_base) { Base b{0x0F}; From 951203bcc496c4415b7754cd764544659b76067f Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Tue, 20 Feb 2024 17:45:47 -0500 Subject: [PATCH 6/7] net: Simplify ParamsStream usage Simplify ParamsStream usage in ConvertSeeds now that ParamsStream supports rvalue substream arguments. --- src/net.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index ba764e28e19..8bb1558ee9c 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -198,8 +198,7 @@ static std::vector ConvertSeeds(const std::vector &vSeedsIn) const auto one_week{7 * 24h}; std::vector vSeedsOut; FastRandomContext rng; - DataStream underlying_stream{vSeedsIn}; - ParamsStream s{underlying_stream, CAddress::V2_NETWORK}; + ParamsStream s{DataStream{vSeedsIn}, CAddress::V2_NETWORK}; while (!s.eof()) { CService endpoint; s >> endpoint; From 8d491ae9ecf1948ea29f67b50ca7259123f602aa Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Wed, 21 Feb 2024 07:35:38 -0500 Subject: [PATCH 7/7] serialization: Add ParamsStream GetStream() method Add GetStream() method useful for accessing underlying stream. Use to improve ParamsStream test coverage. --- src/serialize.h | 32 +++++++++++++++++++++++++++----- src/test/serialize_tests.cpp | 5 ++++- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/serialize.h b/src/serialize.h index 8bca4d8ada1..d3eb1acb1ff 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -1103,6 +1103,10 @@ size_t GetSerializeSize(const T& t) return (SizeComputer() << t).size(); } +//! Check if type contains a stream by seeing if has a GetStream() method. +template +concept ContainsStream = requires(T t) { t.GetStream(); }; + /** Wrapper that overrides the GetParams() function of a stream. */ template class ParamsStream @@ -1126,11 +1130,11 @@ public: template ParamsStream& operator<<(const U& obj) { ::Serialize(*this, obj); return *this; } template ParamsStream& operator>>(U&& obj) { ::Unserialize(*this, obj); return *this; } - void write(Span src) { m_substream.write(src); } - void read(Span dst) { m_substream.read(dst); } - void ignore(size_t num) { m_substream.ignore(num); } - bool eof() const { return m_substream.eof(); } - size_t size() const { return m_substream.size(); } + void write(Span src) { GetStream().write(src); } + void read(Span dst) { GetStream().read(dst); } + void ignore(size_t num) { GetStream().ignore(num); } + bool eof() const { return GetStream().eof(); } + size_t size() const { return GetStream().size(); } //! Get reference to stream parameters. template @@ -1142,6 +1146,24 @@ public: return m_substream.template GetParams

(); } } + + //! Get reference to underlying stream. + auto& GetStream() + { + if constexpr (ContainsStream) { + return m_substream.GetStream(); + } else { + return m_substream; + } + } + const auto& GetStream() const + { + if constexpr (ContainsStream) { + return m_substream.GetStream(); + } else { + return m_substream; + } + } }; /** diff --git a/src/test/serialize_tests.cpp b/src/test/serialize_tests.cpp index ad4852d13d2..b28e1b41966 100644 --- a/src/test/serialize_tests.cpp +++ b/src/test/serialize_tests.cpp @@ -412,11 +412,14 @@ BOOST_AUTO_TEST_CASE(with_params_multi) //! Test creating a ParamsStream that moves from a stream argument. BOOST_AUTO_TEST_CASE(with_params_move) { - UncopyableStream stream{}; + UncopyableStream stream{MakeByteSpan(std::string_view{"abc"})}; ParamsStream pstream{std::move(stream), RAW, HEX, RAW}; + BOOST_CHECK_EQUAL(pstream.GetStream().str(), "abc"); + pstream.GetStream().clear(); Base base1{0x20}; pstream << base1; + BOOST_CHECK_EQUAL(pstream.GetStream().str(), "\x20"); Base base2; pstream >> base2;