If you don't mind 10 bytes of overhead and don't want to use any undocumented interface, use Serialization support.
Otherwise, "hack" the backend implementation.
Using Serialization
E.g.
Compiler Explorer
#include <boost/archive/binary_oarchive.hpp>
#include <boost/multiprecision/cpp_dec_float.hpp>
#include <boost/multiprecision/cpp_int.hpp>
#include <fmt/ranges.h>
#include <sstream>
#include <vector>
#include <span>
using F = boost::multiprecision::cpp_dec_float_50;
namespace ba = boost::archive;
int main() {
F f{"2837498273489289734982739482398426938568923658926938478923748"};
std::vector<unsigned char> raw;
{
std::ostringstream oss;
{
ba::binary_oarchive oa(
oss, ba::no_header | ba::no_codecvt | ba::no_tracking);
oa << f;
}
auto buf = std::move(oss).str();
raw.assign(buf.begin(), buf.end());
}
fmt::print(" sizeof: {} raw {} bytes {::#0x}\n", sizeof(F), raw.size(),
std::span(raw.data(), raw.size()));
}
Prints
sizeof: 56 raw 63 bytes [0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xd6, 0x6e, 0x0, 0x0, 0xd1, 0x88, 0xdb, 0x5, 0xba, 0x19, 0xba, 0x1, 0x7, 0x3, 0xa2, 0x1, 0x3a,
0xe0, 0xdd, 0x5, 0xcd, 0x1b, 0x64, 0x3, 0x88, 0x24, 0x52, 0x5, 0xe4, 0x47, 0xb4, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x
0, 0xa, 0x0, 0x0, 0x0]
Hacking the Backend
Turns out the related stuff is private. But serialize
is generic, so you can use it to exfiltrate the privates:
template <class Archive>
void serialize(Archive& ar, const unsigned int /*version*/)
{
for (unsigned i = 0; i < data.size(); ++i)
ar& boost::make_nvp("digit", data[i]);
ar& boost::make_nvp("exponent", exp);
ar& boost::make_nvp("sign", neg);
ar& boost::make_nvp("class-type", fpclass);
ar& boost::make_nvp("precision", prec_elem);
}
E.g.: Live Compiler Explorer
//#include <boost/core/demangle.hpp>
#include <boost/multiprecision/cpp_dec_float.hpp>
#include <fmt/ranges.h>
#include <vector>
using F = boost::multiprecision::cpp_dec_float_50;
struct Hack {
std::vector<unsigned char> result {};
template <typename T> Hack& operator&(boost::serialization::nvp<T> const& w) {
return operator&(w.value());
}
template <typename, typename = void> struct Serializable : std::false_type{};
template <typename T> struct Serializable<T,
std::void_t<decltype(std::declval<T>().serialize(
std::declval<Hack&>(), 0u))>> : std::true_type {
};
template <typename T> Hack& operator&(T const& v)
{
if constexpr (Serializable<T>{}) {
const_cast<T&>(v).serialize(*this, 0u);
} else {
constexpr size_t n = sizeof(v);
//fmt::print("{} ({} bytes)\n", boost::core::demangle(typeid(v).name()), n);
static_assert(std::is_trivial_v<T>);
static_assert(std::is_standard_layout_v<T>);
auto at = result.size();
result.resize(result.size() + n);
std::memcpy(result.data() + at, &v, n);
}
return *this;
}
};
int main() {
F f{"2837498273489289734982739482398426938568923658926938478923748"};
Hack hack;
f.serialize(hack, 0u);
fmt::print(" sizeof: {} raw {} bytes {::#0x}\n", sizeof(F),
hack.result.size(), hack.result);
}
Prints
sizeof: 56 raw 53 bytes [0xd6, 0x6e, 0x0, 0x0, 0xd1, 0x88, 0xdb, 0x5, 0xba, 0x19, 0xba, 0x1, 0x7, 0x3, 0xa2, 0x1, 0x3a, 0xe0, 0xdd, 0x5, 0xcd, 0x1b, 0x64, 0x3, 0x88, 0x24, 0x52, 0x5, 0xe4, 0x47, 0xb4, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x38, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xa, 0x0, 0x0, 0x0]
Summary / Caveat
I'll leave the corresponding deserialization code as an exercise for the reader.
In the end, the hack approach turns out to be pretty similar to the clean approach, just mocking the serialization archive.
Note that versioning is not supported in the Hack approach.
Also, portability may not be a given for both approaches. Check whether endianness/processor architectures change your requirements.