Giter Site home page Giter Site logo

zpp_bits's Introduction

zpp::bits

.github/workflows/actions.yml Build Status

A modern C++20 binary serialization and RPC library, with just one header file.

This library is a successor to zpp::serializer. The library tries to be simpler for use, but has more or less similar API to its predecessor.

Contents

Motivation

  • Serialize any object from and to binary form as seamless as possible.
  • Provide lightweight remote procedure call (RPC) capabilities
  • Be the fastest possible - see the benchmark

The Difference From zpp::serializer

  • It is simpler
  • Performance improvements
  • Almost everything is constexpr
  • More flexible with serialization of the size of variable length types, opt-out from serializing size.
  • Opt-in for zpp::throwing if header is found.
  • More friendly towards freestanding (no exception runtime support).
  • Breaks compatibility with anything lower than C++20 (which is why the original library is left intact).
  • Better naming for utility classes and namespaces, for instance zpp::bits is more easily typed than zpp::serializer.
  • Modernized polymorphism, based on variant and flexible serialization ids, compared to zpp::serializer global polymorphic types with fixed 8 bytes of sha1 serialization id.
  • Lightweight RPC capabilities

Introduction

For many types, enabling serialization is transparent and requires no additional lines of code. These types are required to be of aggregate type, with non array members. Here is an example of a person class with name and age:

struct person
{
    std::string name;
    int age{};
};

Example how to serialize the person into and from a vector of bytes:

// The `data_in_out` utility function creates a vector of bytes, the input and output archives
// and returns them so we can decompose them easily in one line using structured binding like so:
auto [data, in, out] = zpp::bits::data_in_out();

// Serialize a few people:
out(person{"Person1", 25}, person{"Person2", 35});

// Define our people.
person p1, p2;

// We can now deserialize them either one by one `in(p1)` `in(p2)`, or together, here
// we chose to do it together in one line:
in(p1, p2);

This example almost works, we are being warned that we are discarding the return value. For error checking keep reading.

Error Handling

We need to check for errors, the library offers multiple ways to do so - a return value based, exception based, or zpp::throwing based.

Using return values

The return value based way for being most explicit, or if you just prefer return values:

auto [data, in, out] = zpp::bits::data_in_out();

auto result = out(person{"Person1", 25}, person{"Person2", 35});
if (failure(result)) {
    // `result` is implicitly convertible to `std::errc`.
    // handle the error or return/throw exception.
}

person p1, p2;

result = in(p1, p2);
if (failure(result)) {
    // `result` is implicitly convertible to `std::errc`.
    // handle the error or return/throw exception.
}

Using exceptions

The exceptions based way using .or_throw() (read this as "succeed or throw" - hence or_throw()):

int main()
{
    try {
        auto [data, in, out] = zpp::bits::data_in_out();

        // Check error using `or_throw()` which throws an exception.
        out(person{"Person1", 25}, person{"Person2", 35}).or_throw();

        person p1, p2;

        // Check error using `or_throw()` which throws an exception.
        in(p1, p2).or_throw();

        return 0;
    } catch (const std::exception & error) {
        std::cout << "Failed with error: " << error.what() << '\n';
        return 1;
    } catch (...) {
        std::cout << "Unknown error\n";
        return 1;
    });
}

Using zpp::throwing

Another option is zpp::throwing where error checking turns into two simple co_awaits, to understand how to check for error we provide a full main function:

int main()
{
    return zpp::try_catch([]() -> zpp::throwing<int> {
        auto [data, in, out] = zpp::bits::data_in_out();

        // Check error using `co_await`, which suspends the coroutine.
        co_await out(person{"Person1", 25}, person{"Person2", 35});

        person p1, p2;

        // Check error using `co_await`, which suspends the coroutine.
        co_await in(p1, p2);

        co_return 0;
    }, [](zpp::error error) {
        std::cout << "Failed with error: " << error.message() << '\n';
        return 1;
    }, [](/* catch all */) {
        std::cout << "Unknown error\n";
        return 1;
    });
}

Error Codes

All of the above methods, use the following error codes internally and can be checked using the comparison operator from return value based, or by examining the internal error code of std::system_error or zpp::throwing depending which one you used:

  1. std::errc::result_out_of_range - attempting to write or read from a too short buffer.
  2. std::errc::no_buffer_space - growing buffer would grow beyond the allocation limits or overflow.
  3. std::errc::value_too_large - varint (variable length integer) encoding is beyond the representation limits.
  4. std::errc::message_size - message size is beyond the user defined allocation limits.
  5. std::errc::not_supported - attempt to call an RPC that is not listed as supported.
  6. std::errc::bad_message - attempt to read a variant of unrecognized type.
  7. std::errc::invalid_argument - attempting to serialize null pointer or a value-less variant.
  8. std::errc::protocol_error - attempt to deserialize an invalid protocol message.

Serializing Non-Aggregates

For most non-aggregate types (or aggregate types with array members), enabling serialization is a one liner. Here is an example of a non-aggregate person class:

struct person
{
    // Add this line to your class with the number of members:
    using serialize = zpp::bits::members<2>; // Two members

    person(auto && ...){/*...*/} // Make non-aggregate.

    std::string name;
    int age{};
};

Most of the time types we serialize can work with structured binding, and this library takes advantage of that, but you need to provide the number of members in your class for this to work using the method above.

This also works with argument dependent lookup, allowing to not modify the source class:

namespace my_namespace
{
struct person
{
    person(auto && ...){/*...*/} // Make non-aggregate.

    std::string name;
    int age{};
};

// Add this line somewhere before the actual serialization happens.
auto serialize(const person & person) -> zpp::bits::members<2>;
} // namespace my_namespace

In some compilers, SFINAE works with requires expression under if constexpr and unevaluated lambda expression. It means that even with non aggregate types the number of members can be detected automatically in cases where all members are in the same struct. To opt-in, define ZPP_BITS_AUTODETECT_MEMBERS_MODE=1.

// Members are detected automatically, no additional change needed.
struct person
{
    person(auto && ...){/*...*/} // Make non-aggregate.

    std::string name;
    int age{};
};

This works with clang 13, however the portability of this is not clear, since in gcc it does not work (it is a hard error) and it explicitly states in the standard that there is intent not to allow SFINAE in similar cases, so it is turned off by default.

Serializing Private Classes

If your data members or default constructor are private, you need to become friend with zpp::bits::access like so:

struct private_person
{
    // Add this line to your class.
    friend zpp::bits::access;
    using serialize = zpp::bits::members<2>;

private:
    std::string name;
    int age{};
};

Explicit Serialization

To enable save & load of any object using explicit serialization, which works regardless of structured binding compatibility, add the following lines to your class:

    constexpr static auto serialize(auto & archive, auto & self)
    {
        return archive(self.object_1, self.object_2, ...);
    }

Note that object_1, object_2, ... are the non-static data members of your class.

Here is the example of a person class again with explicit serialization function:

struct person
{
    constexpr static auto serialize(auto & archive, auto & self)
    {
        return archive(self.name, self.age);
    }

    std::string name;
    int age{};
};

Or with argument dependent lookup:

namespace my_namespace
{
struct person
{
    std::string name;
    int age{};
};

constexpr auto serialize(auto & archive, person & person)
{
    return archive(person.name, person.age);
}

constexpr auto serialize(auto & archive, const person & person)
{
    return archive(person.name, person.age);
}
} // namespace my_namespace

Archive Creation

Creating input and output archives together and separately from data:

// Create both a vector of bytes, input and output archives.
auto [data, in, out] = zpp::bits::data_in_out();

// Create just the input and output archives, and bind them to the
// existing vector of bytes.
std::vector<std::byte> data;
auto [in, out] = zpp::bits::in_out(data);

// Create all of them separately
std::vector<std::byte> data;
zpp::bits::in in(data);
zpp::bits::out out(data);

// When you need just data and in/out
auto [data, in] = zpp::bits::data_in();
auto [data, out] = zpp::bits::data_out();

Archives can be created from either one of the byte types:

// Either one of these work with the below.
std::vector<std::byte> data;
std::vector<char> data;
std::vector<unsigned char> data;
std::string data;

// Automatically works with either `std::byte`, `char`, `unsigned char`.
zpp::bits::in in(data);
zpp::bits::out out(data);

You can also use fixed size data objects such as array, std::array and view types such as std::span similar to the above. You just need to make sure there is enough size since they are non resizable:

// Either one of these work with the below.
std::byte data[0x1000];
char data[0x1000];
unsigned char data[0x1000];
std::array<std::byte, 0x1000> data;
std::array<char, 0x1000> data;
std::array<unsigned char, 0x1000> data;
std::span<std::byte> data = /*...*/;
std::span<char> data = /*...*/;
std::span<unsigned char> data = /*...*/;

// Automatically works with either `std::byte`, `char`, `unsigned char`.
zpp::bits::in in(data);
zpp::bits::out out(data);

When using a vector or string, it automatically grows to the right size, however, with the above the data is limited to the boundaries of the arrays or spans.

When creating the archive in any of the ways above, it is possible to pass a variadic number of parameters that control the archive behavior, such as for byte order, default size types, specifying append behavior and so on. This is discussed in the rest of the README.

Constexpr Serialization

As was said above, the library is almost completely constexpr, here is an example of using array as data object but also using it in compile time to serialize and deserialize a tuple of integers:

constexpr auto tuple_integers()
{
    std::array<std::byte, 0x1000> data{};
    auto [in, out] = zpp::bits::in_out(data);
    out(std::tuple{1,2,3,4,5}).or_throw();

    std::tuple t{0,0,0,0,0};
    in(t).or_throw();
    return t;
}

// Compile time check.
static_assert(tuple_integers() == std::tuple{1,2,3,4,5});

For convenience, the library also provides some simplified serialization functions for compile time:

using namespace zpp::bits::literals;

// Returns an array
// where the first bytes are those of the hello world string and then
// the 1337 as 4 byte integer.
constexpr std::array data =
    zpp::bits::to_bytes<"Hello World!"_s, 1337>();

static_assert(
    zpp::bits::from_bytes<data,
                          zpp::bits::string_literal<char, 12>,
                          int>() == std::tuple{"Hello World!"_s, 1337});

Position Control

Query the position of in and out using position(), in other words the bytes read and written respectively:

std::size_t bytes_read = in.position();
std::size_t bytes_written = out.position();

Reset the position backwards or forwards, or to the beginning, use with extreme care:

in.reset(); // reset to beginning.
in.reset(position); // reset to position.
in.position() -= sizeof(int); // Go back an integer.
in.position() += sizeof(int); // Go forward an integer.

out.reset(); // reset to beginning.
out.reset(position); // reset to position.
out.position() -= sizeof(int); // Go back an integer.
out.position() += sizeof(int); // Go forward an integer.

Standard Library Types Serialization

When serializing variable length standard library types, such as vectors, strings and view types such as span and string view, the library first stores 4 byte integer representing the size, followed by the elements.

std::vector v = {1,2,3,4};
out(v);
in(v);

The reason why the default size type is of 4 bytes (i.e std::uint32_t) is for portability between different architectures, as well as most programs almost never reach a case of a container being more than 2^32 items, and it may be unjust to pay the price of 8 bytes size by default.

For specific size types that are not 4 bytes, use zpp::bits::sized/zpp::bits::sized_t like so:

// Using `sized` function:
std::vector<int> v = {1,2,3,4};
out(zpp::bits::sized<std::uint16_t>(v));
in(zpp::bits::sized<std::uint16_t>(v));

// Using `sized_t` type:
zpp::bits::sized_t<std::vector<int>, std::uint16_t> v = {1,2,3,4};
out(v);
in(v);

Make sure that the size type is large enough for the serialized object, otherwise less items will be serialized, according to conversion rules of unsigned types.

You can also choose to not serialize the size at all, like so:

// Using `unsized` function:
std::vector<int> v = {1,2,3,4};
out(zpp::bits::unsized(v));
in(zpp::bits::unsized(v));

// Using `unsized_t` type:
zpp::bits::unsized_t<std::vector<int>> v = {1,2,3,4};
out(v);
in(v);

For where it is common, there are alias declarations for sized / unsized versions of types, for example, here are vector and span, others such as string, string_view, etc are using the same pattern.

zpp::bits::vector1b<T>; // vector with 1 byte size.
zpp::bits::vector2b<T>; // vector with 2 byte size.
zpp::bits::vector4b<T>; // vector with 4 byte size == default std::vector configuration
zpp::bits::vector8b<T>; // vector with 8 byte size.
zpp::bits::static_vector<T>; // unsized vector
zpp::bits::native_vector<T>; // vector with native (size_type) byte size.

zpp::bits::span1b<T>; // span with 1 byte size.
zpp::bits::span2b<T>; // span with 2 byte size.
zpp::bits::span4b<T>; // span with 4 byte size == default std::span configuration
zpp::bits::span8b<T>; // span with 8 byte size.
zpp::bits::static_span<T>; // unsized span
zpp::bits::native_span<T>; // span with native (size_type) byte size.

Serialization of fixed size types such as arrays, std::arrays, std::tuples don't include any overhead except the elements followed by each other.

Changing the default size type for the whole archive is possible during creation:

zpp::bits::in in(data, zpp::bits::size1b{}); // Use 1 byte for size.
zpp::bits::out out(data, zpp::bits::size1b{}); // Use 1 byte for size.

zpp::bits::in in(data, zpp::bits::size2b{}); // Use 2 bytes for size.
zpp::bits::out out(data, zpp::bits::size2b{}); // Use 2 bytes for size.

zpp::bits::in in(data, zpp::bits::size4b{}); // Use 4 bytes for size.
zpp::bits::out out(data, zpp::bits::size4b{}); // Use 4 bytes for size.

zpp::bits::in in(data, zpp::bits::size8b{}); // Use 8 bytes for size.
zpp::bits::out out(data, zpp::bits::size8b{}); // Use 8 bytes for size.

zpp::bits::in in(data, zpp::bits::size_native{}); // Use std::size_t for size.
zpp::bits::out out(data, zpp::bits::size_native{}); // Use std::size_t for size.

zpp::bits::in in(data, zpp::bits::no_size{}); // Don't use size, for very special cases, since it is very limiting.
zpp::bits::out out(data, zpp::bits::no_size{}); // Don't use size, for very special cases, since it is very limiting.

// Can also do it together, for example for 2 bytes size:
auto [data, in, out] = data_in_out(zpp::bits::size2b{});
auto [data, out] = data_out(zpp::bits::size2b{});
auto [data, in] = data_in(zpp::bits::size2b{});

Serialization as Bytes

Most of the types the library knows how to optimize and serialize objects as bytes. It is however disabled when using explicit serialization functions.

If you know your type is serializable just as raw bytes, and you are using explicit serialization, you can opt in and optimize its serialization to a mere memcpy:

struct point
{
    int x;
    int y;

    constexpr static auto serialize(auto & archive, auto & self)
    {
        // Serialize as bytes, instead of serializing each
        // member separately. The overall result is the same, but this may be
        // faster sometimes.
        return archive(zpp::bits::as_bytes(self));
    }
};

It's also possible to do this directly from a vector or span of trivially copyable types, this time we use bytes instead of as_bytes because we convert the contents of the vector to bytes rather than the vector object itself (the data the vector points to rather than the vector object):

std::vector<point> points;
out(zpp::bits::bytes(points));
in(zpp::bits::bytes(points));

However in this case the size is not serialized, this may be extended in the future to also support serializing the size similar to other view types. If you need to serialize as bytes and want the size, as a workaround it's possible to cast to std::span<std::byte>.

Variant Types and Version Control

While there is no perfect tool to handle backwards compatibility of structures because of the zero overhead-ness of the serialization, you can use std::variant as a way to version your classes or create a nice polymorphism based dispatching, here is how:

namespace v1
{
struct person
{
    using serialize = zpp::bits::members<2>;

    auto get_hobby() const
    {
        return "<none>"sv;
    }

    std::string name;
    int age;
};
} // namespace v1

namespace v2
{
struct person
{
    using serialize = zpp::bits::members<3>;

    auto get_hobby() const
    {
        return std::string_view(hobby);
    }

    std::string name;
    int age;
    std::string hobby;
};
} // namespace v2

And then to the serialization itself:

auto [data, in, out] = zpp::bits::data_in_out();
out(std::variant<v1::person, v2::person>(v1::person{"Person1", 25}))
    .or_throw();

std::variant<v1::person, v2::person> v;
in(v).or_throw();

std::visit([](auto && person) {
    (void) person.name == "Person1";
    (void) person.age == 25;
    (void) person.get_hobby() == "<none>";
}, v);

out(std::variant<v1::person, v2::person>(
        v2::person{"Person2", 35, "Basketball"}))
    .or_throw();

in(v).or_throw();

std::visit([](auto && person) {
    (void) person.name == "Person2";
    (void) person.age == 35;
    (void) person.get_hobby() == "Basketball";
}, v);

The way the variant gets serialized is by serializing its index (0 or 1) as a std::byte before serializing the actual object. This is very efficient, however sometimes users may want to choose explicit serialization id for that, refer to the point below

To set a custom serialization id, you need to add an additional line inside/outside your class respectively:

using namespace zpp::bits::literals;

// Inside the class, this serializes the full string "v1::person" before you serialize
// the person.
using serialize_id = zpp::bits::id<"v1::person"_s>;

// Outside the class, this serializes the full string "v1::person" before you serialize
// the person.
auto serialize_id(const person &) -> zpp::bits::id<"v1::person"_s>;

Note that the serialization ids of types in the variant must match in length, or a compilation error will issue.

You may also use any sequence of bytes instead of a readable string, as well as an integer or any literal type, here is an example of how to use a hash of a string as a serialization id:

using namespace zpp::bits::literals;

// Inside:
using serialize_id = zpp::bits::id<"v1::person"_sha1>; // Sha1
using serialize_id = zpp::bits::id<"v1::person"_sha256>; // Sha256

// Outside:
auto serialize_id(const person &) -> zpp::bits::id<"v1::person"_sha1>; // Sha1
auto serialize_id(const person &) -> zpp::bits::id<"v1::person"_sha256>; // Sha256

You can also serialize just the first bytes of the hash, like so:

// First 4 bytes of hash:
using serialize_id = zpp::bits::id<"v1::person"_sha256, 4>;

// First sizeof(int) bytes of hash:
using serialize_id = zpp::bits::id<"v1::person"_sha256_int>;

The type is then converted to bytes at compile time using (... wait for it) zpp::bits::out at compile time, so as long as your literal type is serializable according to the above, you can use it as a serialization id. The id is serialized to std::array<std::byte, N> however for 1, 2, 4, and 8 bytes its underlying type is std::byte std::uint16_t, std::uin32_t and std::uint64_t respectively for ease of use and efficiency.

If you want to serialize the variant without an id, or if you know that a variant is going to have a particular ID upon deserialize, you may do it using zpp::bits::known_id to wrap your variant:

std::variant<v1::person, v2::person> v;

 // Id assumed to be v2::person, and is not serialized / deserialized.
out(zpp::bits::known_id<"v2::person"_sha256_int>(v));
in(zpp::bits::known_id<"v2::person"_sha256_int>(v));

// When deserializing you can pass the id as function parameter, to be able
// to use outside of compile time context. `id_v` stands for "id value".
// In our case 4 bytes translates to a plain std::uint32_t, so any dynamic
// integer could fit as the first parameter to `known_id` below.
in(zpp::bits::known_id(zpp::bits::id_v<"v2::person"_sha256_int>, v));

Literal Operators

Description of helper literals in the library:

using namespace zpp::bits::literals;

"hello"_s // Make a string literal.
"hello"_b // Make a binary data literal.
"hello"_sha1 // Make a sha1 binary data literal.
"hello"_sha256 // Make a sha256 binary data literal.
"hello"_sha1_int // Make a sha1 integer from the first hash bytes.
"hello"_sha256_int // Make a sha256 integer from the first hash bytes.
"01020304"_decode_hex // Decode a hex string into bytes literal.

Apply to Functions

  • You can apply input archive contents to a function directly, using zpp::bits::apply, the function must be non-template and have exactly one overload:
int foo(std::string s, int i)
{
    // s == "hello"s;
    // i == 1337;
    return 1338;
}

auto [data, in, out] = zpp::bits::data_in_out();
out("hello"s, 1337).or_throw();

// Call the foo in one of the following ways:

// Exception based:
zpp::bits::apply(foo, in).or_throw() == 1338;

// zpp::throwing based:
co_await zpp::bits::apply(foo, in) == 1338;

// Return value based:
if (auto result = zpp::bits::apply(foo, in);
    failure(result)) {
    // Failure...
} else {
    result.value() == 1338;
}

When your function receives no parameters, the effect is just calling the function without deserialization and the return value is the return value of your function. When the function returns void, there is no value for the resulting type.

Remote Procedure Call (RPC)

The library also provides a thin RPC (remote procedure call) interface to allow serializing and deserializing function calls:

using namespace std::literals;
using namespace zpp::bits::literals;

int foo(int i, std::string s);
std::string bar(int i, int j);

using rpc = zpp::bits::rpc<
    zpp::bits::bind<foo, "foo"_sha256_int>,
    zpp::bits::bind<bar, "bar"_sha256_int>
>;

auto [data, in, out] = zpp::bits::data_in_out();

// Server and client together:
auto [client, server] = rpc::client_server(in, out);

// Or separately:
rpc::client client{in, out};
rpc::server server{in, out};

// Request from the client:
client.request<"foo"_sha256_int>(1337, "hello"s).or_throw();

// Serve the request from the server:
server.serve().or_throw();

// Read back the response
client.response<"foo"_sha256_int>().or_throw(); // == foo(1337, "hello"s);

Regarding error handling, similar to many examples above you can use return value, exceptions, or zpp::throwing way for handling errors.

// Return value based.
if (auto result = client.request<"foo"_sha256_int>(1337, "hello"s); failure(result)) {
    // Handle the failure.
}
if (auto result = server.serve(); failure(result)) {
    // Handle the failure.
}
if (auto result = client.response<"foo"_sha256_int>(); failure(result)) {
    // Handle the failure.
} else {
    // Use response.value();
}

// Throwing based.
co_await client.request<"foo"_sha256_int>(1337, "hello"s); failure(result));
co_await server.serve();
co_await client.response<"foo"_sha256_int>(); // == foo(1337, "hello"s);

It's possible for the IDs of the RPC calls to be skipped, for example of they are passed out of band, here is how to achieve this:

server.serve(id); // id is already known, don't deserialize it.
client.request_body<Id>(arguments...); // request without serializing id.

Member functions can also be registered for RPC, however the server needs to get a reference to the class object during construction, and all of the member functions must belong to the same class (though namespace scope functions are ok to mix):

struct a
{
    int foo(int i, std::string s);
};

std::string bar(int i, int j);

using rpc = zpp::bits::rpc<
    zpp::bits::bind<&a::foo, "a::foo"_sha256_int>,
    zpp::bits::bind<bar, "bar"_sha256_int>
>;

auto [data, in, out] = zpp::bits::data_in_out();

// Our object.
a a1;

// Server and client together:
auto [client, server] = rpc::client_server(in, out, a1);

// Or separately:
rpc::client client{in, out};
rpc::server server{in, out, a1};

// Request from the client:
client.request<"a::foo"_sha256_int>(1337, "hello"s).or_throw();

// Serve the request from the server:
server.serve().or_throw();

// Read back the response
client.response<"a::foo"_sha256_int>().or_throw(); // == a1.foo(1337, "hello"s);

The RPC can also work in an opaque mode and let the function itself serialize/deserialize the data, when binding a function as opaque, using bind_opaque:

// Each of the following signatures of `foo()` are valid for opaque rpc call:
auto foo(zpp::bits::in<> &, zpp::bits::out<> &);
auto foo(zpp::bits::in<> &);
auto foo(zpp::bits::out<> &);
auto foo(std::span<std::byte> input); // assumes all data is consumed from archive.
auto foo(std::span<std::byte> & input); // resize input in the function to signal how much was consumed.

using rpc = zpp::bits::rpc<
    zpp::bits::bind_opaque<foo, "a::foo"_sha256_int>,
    zpp::bits::bind<bar, "bar"_sha256_int>
>;

Byte Order Customization

The default byte order used is the native processor/OS selected one. You may choose another byte order using zpp::bits::endian during construction like so:

zpp::bits::in in(data, zpp::bits::endian::big{}); // Use big endian
zpp::bits::out out(data, zpp::bits::endian::big{}); // Use big endian

zpp::bits::in in(data, zpp::bits::endian::network{}); // Use big endian (provided for convenience)
zpp::bits::out out(data, zpp::bits::endian::network{}); // Use big endian (provided for convenience)

zpp::bits::in in(data, zpp::bits::endian::little{}); // Use little endian
zpp::bits::out out(data, zpp::bits::endian::little{}); // Use little endian

zpp::bits::in in(data, zpp::bits::endian::swapped{}); // If little use big otherwise little.
zpp::bits::out out(data, zpp::bits::endian::swapped{}); // If little use big otherwise little.

zpp::bits::in in(data, zpp::bits::endian::native{}); // Use the native one (default).
zpp::bits::out out(data, zpp::bits::endian::native{}); // Use the native one (default).

// Can also do it together, for example big endian:
auto [data, in, out] = data_in_out(zpp::bits::endian::big{});
auto [data, out] = data_out(zpp::bits::endian::big{});
auto [data, in] = data_in(zpp::bits::endian::big{});

Deserializing Views Of Const Bytes

On the receiving end (input archive), the library supports view types of const byte types, such as std::span<const std::byte> in order to get a view at a portion of data without copying. This needs to be carefully used because invalidating iterators of the contained data could cause a use after free. It is provided to allow the optimization when needed:

using namespace std::literals;

auto [data, in, out] = zpp::bits::data_in_out();
out("hello"sv).or_throw();

std::span<const std::byte> s;
in(s).or_throw();

// s.size() == "hello"sv.size()
// std::memcmp("hello"sv.data(), s.data(), "hello"sv.size()) == 0
}

There is also an unsized version, which consumes the rest of the archive data to allow the common use case of header then arbitrary amount of data:

auto [data, in, out] = zpp::bits::data_in_out();
out(zpp::bits::unsized("hello"sv)).or_throw();

std::span<const std::byte> s;
in(zpp::bits::unsized(s)).or_throw();

// s.size() == "hello"sv.size()
// std::memcmp("hello"sv.data(), s.data(), "hello"sv.size()) == 0

Pointers as Optionals

The library does not support serializing null pointer values, however to explicitly support optional owning pointers, such as to create graphs and complex structures.

In theory it's valid to use std::optional<std::unique_ptr<T>>, but it's recommended to use the specifically made zpp::bits::optional_ptr<T> which optimizes out the boolean that the optional object usually keeps, and uses null pointer as an invalid state.

Serializing a null pointer value in that case will serialize a zero byte, while non-null values serialize as a single one byte followed by the bytes of the object. (i.e, serialization is identical to std::optional<T>).

Reflection

As part of the library implementation it was required to implement some reflection types, for counting members and visiting members, and the library exposes these to the user:

struct point
{
    int x;
    int y;
};

#if !ZPP_BITS_AUTODETECT_MEMBERS_MODE
auto serialize(point) -> zpp::bits::members<2>;
#endif

static_assert(zpp::bits::number_of_members<point>() == 2);

constexpr auto sum = zpp::bits::visit_members(
    point{1, 2}, [](auto x, auto y) { return x + y; });

static_assert(sum == 3);

constexpr auto generic_sum = zpp::bits::visit_members(
    point{1, 2}, [](auto... members) { return (0 + ... + members); });

static_assert(generic_sum == 3);

constexpr auto is_two_integers =
    zpp::bits::visit_members_types<point>([]<typename... Types>() {
        if constexpr (std::same_as<std::tuple<Types...>,
                                   std::tuple<int, int>>) {
            return std::true_type{};
        } else {
            return std::false_type{};
        }
    })();

static_assert(is_two_integers);

The example above works with or without ZPP_BITS_AUTODETECT_MEMBERS_MODE=1, depending on the #if. As noted above, we must rely on specific compiler feature to detect the number of members which may not be portable.

Additional Archive Controls

Archives can be constructed with additional control options such as zpp::bits::append{} which instructs output archives to set the position to the end of the vector or other data source. (for input archives this option has no effect)

std::vector<std::byte> data;
zpp::bits::out out(data, zpp::bits::append{});

It is possible to use multiple controls and to use them also with data_in_out/data_in/data_out/in_out:

zpp::bits::out out(data, zpp::bits::append{}, zpp::bits::endian::big{});
auto [in, out] = in_out(data, zpp::bits::append{}, zpp::bits::endian::big{});
auto [data, in, out] = data_in_out(zpp::bits::size2b{}, zpp::bits::endian::big{});

Allocation size can be limited in case of output archive to a growing buffer or when using an input archive to limit how long a single length prefixed message can be to avoid allocation of a very large buffer in advance, using zpp::bits::alloc_limit<L>{}. The intended use is for safety and sanity reasons rather than accurate allocation measurement:

zpp::bits::out out(data, zpp::bits::alloc_limit<0x10000>{});
zpp::bits::in in(data, zpp::bits::alloc_limit<0x10000>{});
auto [in, out] = in_out(data, zpp::bits::alloc_limit<0x10000>{});
auto [data, in, out] = data_in_out(zpp::bits::alloc_limit<0x10000>{});

For best correctness, when using growing buffer for output, if the buffer was grown, the buffer is resized in the end for the exact position of the output archive, this incurs an extra resize which in most cases is acceptable, but you may avoid this additional resize and recognize the end of the buffer by using position(). You can achieve this by using zpp::bits::no_fit_size{}:

zpp::bits::out out(data, zpp::bits::no_fit_size{});

To control enlarging of output archive vector, you may use zpp::bits::enlarger<Mul, Div = 1>:

zpp::bits::out out(data, zpp::bits::enlarger<2>{}); // Grow by multiplying size by 2.
zpp::bits::out out(data, zpp::bits::enlarger<3, 2>{}); // Default - Grow by multiplying size by 3 and divide by 2 (enlarge by 1.5).
zpp::bits::out out(data, zpp::bits::exact_enlarger{}); // Grow to exact size every time.

By default, for safety, an output archive that uses a growing buffer, checks for overflow before any buffer grow. For 64 bit systems, this check although cheap, is almost redundant, as it is almost impossible to overflow a 64 bit integer when it represents a memory size. (i.e, the memory allocation will fail before the memory comes close to overflow this integer). If you wish to disable those overflow checks, in favor of performance, use: zpp::bits::no_enlarge_overflow{}:

zpp::bits::out out(data, zpp::bits::no_enlarge_overflow{}); // Disable overflow check when enlarging.

When serializing explicitly it is often required to identify whether the archive is input or output archive, and it is done via the archive.kind() static member function, and can be done in an if constexpr:

static constexpr auto serialize(auto & archive, auto & self)
{
    using archive_type = std::remove_cvref_t<decltype(archive)>;

    if constexpr (archive_type::kind() == zpp::bits::kind::in) {
        // Input archive
    } else if constexpr (archive_type::kind() == zpp::bits::kind::out) {
        // Output archive
    } else {
        // No such archive (no need to check for this)
    }
}

Variable Length Integers

The library provides a type for serializing and deserializing variable length integers:

auto [data, in, out] = zpp::bits::data_in_out();
out(zpp::bits::varint{150}).or_throw();

zpp::bits::varint i{0};
in(i).or_throw();

// i == 150;

Here is an example of the encoding at compile time:

static_assert(zpp::bits::to_bytes<zpp::bits::varint{150}>() == "9601"_decode_hex);

The class template zpp::bits::varint<T, E = varint_encoding::normal> is provided to be able to define any varint integral type or enumeration type, along with possible encodings zpp::bits::varint_encoding::normal/zig_zag (normal is default).

The following alias declarations are provided:

using vint32_t = varint<std::int32_t>; // varint of int32 types.
using vint64_t = varint<std::int64_t>; // varint of int64 types.

using vuint32_t = varint<std::uint32_t>; // varint of unsigned int32 types.
using vuint64_t = varint<std::uint64_t>; // varint of unsigned int64 types.

using vsint32_t = varint<std::int32_t, varint_encoding::zig_zag>; // zig zag encoded varint of int32 types.
using vsint64_t = varint<std::int64_t, varint_encoding::zig_zag>; // zig zag encoded varint of int64 types.

using vsize_t = varint<std::size_t>; // varint of std::size_t types.

Using varints to serialize sizes by default is also possible during archive creation:

auto [data, in, out] = data_in_out(zpp::bits::size_varint{});

zpp::bits::in in(data, zpp::bits::size_varint{}); // Uses varint to encode size.
zpp::bits::out out(data, zpp::bits::size_varint{}); // Uses varint to encode size.

Protobuf

The serialization format of this library is not based on any known or accepted format. Naturally, other languages do not support this format, which makes it near impossible to use the library for cross programming language communication.

For this reason the library supports the protobuf format which is available in many languages.

Please note that protobuf support is kind of experimental, which means it may not include every possible protobuf feature, and it is generally slower (around 2-5 times slower, mostly on deserialization) than the default format, which aims to be zero overhead.

Starting with the basic message:

struct example
{
    zpp::bits::vint32_t i; // varint of 32 bit, field number is implicitly set to 1,
    // next field is implicitly 2, and so on
};

// Serialize as protobuf protocol (as usual, can also define this inside the class
// with `using serialize = zpp::bits::pb_protocol;`)
auto serialize(const example &) -> zpp::bits::pb_protocol;

// Use archives as usual, specify what kind of size to prefix the message with.
// We chose no size to demonstrate the actual encoding of the message, but in general
// it is recommended to size prefix protobuf messages since they are not self terminating.
auto [data, in, out] = data_in_out(zpp::bits::no_size{});

out(example{.i = 150}).or_throw();

example e;
in(e).or_throw();

// e.i == 150

// Serialize the message without any size prefix, and check the encoding at compile time:
static_assert(
    zpp::bits::to_bytes<zpp::bits::unsized_t<example>{{.i = 150}}>() ==
    "089601"_decode_hex);

For the full syntax, which we'll later use to pass more options, use zpp::bits::protocol:

// Serialize as protobuf protocol (as usual, can also define this inside the class
// with `using serialize = zpp::bits::protocol<zpp::bits::pb{}>;`)
auto serialize(const example &) -> zpp::bits::protocol<zpp::bits::pb{}>;

To reserve fields:

struct example
{
    [[no_unique_address]] zpp::bits::pb_reserved _1; // field number 1 is reserved.
    zpp::bits::vint32_t i; // field number == 2
    zpp::bits::vsint32_t j; // field number == 3
};

To explicitly specify for each member the field number:

struct example
{
    zpp::bits::pb_field<zpp::bits::vint32_t, 20> i; // field number == 20
    zpp::bits::pb_field<zpp::bits::vsint32_t, 30> j; // field number == 30

    using serialize = zpp::bits::pb_protocol;
};

Accessing the value behind the field is often transparent however if explicitly needed use pb_value(<variable>) to get or assign to the value.

To map members to another field number:

struct example
{
    zpp::bits::vint32_t i; // field number == 20
    zpp::bits::vsint32_t j; // field number == 30

    using serialize = zpp::bits::protocol<
        zpp::bits::pb{
            zpp::bits::pb_map<1, 20>{}, // Map first member to field number 20.
            zpp::bits::pb_map<2, 30>{}}>; // Map second member to field number 30.
};

Fixed members are simply regular C++ data members:

struct example
{
    std::uint32_t i; // fixed unsigned integer 32, field number == 1
};

Like with zpp::bits::members, for when it is required, you may specify the number of members in the protocol field with zpp::bits::pb_members<N>:

struct example
{
    using serialize = zpp::bits::pb_members<1>; // 1 member.

    zpp::bits::vint32_t i; // field number == 1
};

The full version of the above involves passing the number of members as the second parameter to the protocol:

struct example
{
    using serialize = zpp::bits::protocol<zpp::bits::pb{}, 1>; // 1 member.

    zpp::bits::vint32_t i; // field number == 1
};

Embedded messages are simply nested within the class as data members:

struct nested_example
{
    example nested; // field number == 1
};

auto serialize(const nested_example &) -> zpp::bits::pb_protocol;

static_assert(zpp::bits::to_bytes<zpp::bits::unsized_t<nested_example>{
                  {.nested = example{150}}}>() == "0a03089601"_decode_hex);

Repeated fields are of the form of owning containers:

struct repeating
{
    using serialize = zpp::bits::pb_protocol;

    std::vector<zpp::bits::vint32_t> integers; // field number == 1
    std::string characters; // field number == 2
    std::vector<example> examples; // repeating examples, field number == 3
};

Currently all of the fields are optional, which is a good practice, missing fields are dropped and not concatenated to the message, for efficiency. Any value that is not set in a message leaves the target data member intact, which allows to implement defaults for data members by using non-static data member initializer or to initialize the data member before deserializing the message.

Lets take a full .proto file and translate it:

syntax = "proto3";

package tutorial;

message person {
  string name = 1;
  int32 id = 2;
  string email = 3;

  enum phone_type {
    mobile = 0;
    home = 1;
    work = 2;
  }

  message phone_number {
    string number = 1;
    phone_type type = 2;
  }

  repeated phone_number phones = 4;
}

message address_book {
  repeated person people = 1;
}

The translated file:

struct person
{
    std::string name; // = 1
    zpp::bits::vint32_t id; // = 2
    std::string email; // = 3

    enum phone_type
    {
        mobile = 0,
        home = 1,
        work = 2,
    };

    struct phone_number
    {
        std::string number; // = 1
        phone_type type; // = 2
    };

    std::vector<phone_number> phones; // = 4
};

struct address_book
{
    std::vector<person> people; // = 1
};

auto serialize(const person &) -> zpp::bits::pb_protocol;
auto serialize(const person::phone_number &) -> zpp::bits::pb_protocol;
auto serialize(const address_book &) -> zpp::bits::pb_protocol;

Derserializing a message that was originally serialized with python:

import addressbook_pb2
person = addressbook_pb2.person()
person.id = 1234
person.name = "John Doe"
person.email = "[email protected]"
phone = person.phones.add()
phone.number = "555-4321"
phone.type = addressbook_pb2.person.home

The output we get for person is:

name: "John Doe"
id: 1234
email: "[email protected]"
phones {
  number: "555-4321"
  type: home
}

Lets serialize it:

person.SerializeToString()

The result is:

b'\n\x08John Doe\x10\xd2\t\x1a\x10[email protected]"\x0c\n\x08555-4321\x10\x01'

Back to C++:

using namespace zpp::bits::literals;

constexpr auto data =
    "\n\x08John Doe\x10\xd2\t\x1a\x10[email protected]\"\x0c\n\x08"
    "555-4321\x10\x01"_b;
static_assert(data.size() == 45);

person p;
zpp::bits::in{data, zpp::bits::no_size{}}(p).or_throw();

// p.name == "John Doe"
// p.id == 1234
// p.email == "[email protected]"
// p.phones.size() == 1
// p.phones[0].number == "555-4321"
// p.phones[0].type == person::home

Advanced Controls

By default zpp::bits inlines aggressively, but to reduce code size, it does not inline the full decoding of varints (variable length integers). To configure inlining of the full varint decoding, define ZPP_BITS_INLINE_DECODE_VARINT=1.

If you suspect that zpp::bits is inlining too much to the point where it badly affects code size, you may define ZPP_BITS_INLINE_MODE=0, which disables all force inlining and observe the results. Usually it has a negligible effect, but it is provided as is for additional control.

In some compilers, you may find always inline to fail with recursive structures (for example a tree graph). In these cases it is required to somehow avoid the always inline attribute for the specific structure, a trivial example would be to use an explicit serialization function, although most times the library detects such occasions and it is not necessary, but the example is provided just in case:

struct node
{
    constexpr static auto serialize(auto & archive, auto & node)
    {
        return archive(node.value, node.nodes);
    }

    int value;
    std::vector<node> nodes;
};

Benchmark

GCC 11

library test case bin size data size ser time des time
zpp_bits general 52192B 8413B 733ms 693ms
zpp_bits fixed buffer 48000B 8413B 620ms 667ms
bitsery general 70904B 6913B 1470ms 1524ms
bitsery fixed buffer 53648B 6913B 927ms 1466ms
boost general 279024B 11037B 15126ms 12724ms
cereal general 70560B 10413B 10777ms 9088ms
flatbuffers general 70640B 14924B 8757ms 3361ms
handwritten general 47936B 10413B 1506ms 1577ms
handwritten unsafe 47944B 10413B 1616ms 1392ms
iostream general 53872B 8413B 11956ms 12928ms
msgpack general 89144B 8857B 2770ms 14033ms
protobuf general 2077864B 10018B 19929ms 20592ms
protobuf arena 2077872B 10018B 10319ms 11787ms
yas general 61072B 10463B 2286ms 1770ms

Clang 12.0.1

library test case bin size data size ser time des time
zpp_bits general 47128B 8413B 790ms 715ms
zpp_bits fixed buffer 43056B 8413B 605ms 694ms
bitsery general 53728B 6913B 2128ms 1832ms
bitsery fixed buffer 49248B 6913B 946ms 1941ms
boost general 237008B 11037B 16011ms 13017ms
cereal general 61480B 10413B 9977ms 8565ms
flatbuffers general 62512B 14924B 9812ms 3472ms
handwritten general 43112B 10413B 1391ms 1321ms
handwritten unsafe 43120B 10413B 1393ms 1212ms
iostream general 48632B 8413B 10992ms 12771ms
msgpack general 77384B 8857B 3563ms 14705ms
protobuf general 2032712B 10018B 18125ms 20211ms
protobuf arena 2032760B 10018B 9166ms 11378ms
yas general 51000B 10463B 2114ms 1558ms

Limitations

  • Serialization of non-owning pointers & raw pointers is not supported, for simplicity and also for security reasons.
  • Serialization of null pointers is not supported to avoid the default overhead of stating whether a pointer is null, to work around this use optional which is more explicit.

Final Words

I wish that you find this library useful. Please feel free to submit any issues, make suggestions for improvements, etc.

zpp_bits's People

Contributors

asolwa avatar eyalz800 avatar matthew-mcraven avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

zpp_bits's Issues

Implicit conversion changes signedness warning on serialising QByteArray

Hi,
i'm pleasantly surprised this beast can serialised QByteArrays out of the box. I understand that it's using std::vector's interface for that. However, there is a warning:

test_zppbits.cpp:41:12: Implicit conversion changes signedness: 'qsizetype' (aka 'long long') to 'std::size_t' (aka 'unsigned long')
zpp_bits.h:1960:27: in instantiation of function template specialization 'zpp::bits::basic_out<std::vector<std::byte>>::serialize_one<unsigned int, QByteArray &>' requested here
zpp_bits.h:2109:28: in instantiation of function template specialization 'zpp::bits::basic_out<std::vector<std::byte>>::serialize_many<QByteArray &, int &, int &>' requested here
zpp_bits.h:214:559: in instantiation of function template specialization 'zpp::bits::basic_out<std::vector<std::byte>>::serialize_one((anonymous namespace)::TestStruct &)::(anonymous class)::operator()<QByteArray &, int &, int &>' requested here
zpp_bits.h:1407:20: in instantiation of function template specialization 'zpp::bits::access::visit_members<(anonymous namespace)::TestStruct &, (lambda at /home/madam/Documents/work/tuw/alpinemaps/build-renderer-Desktop_Qt_6_5_0_GCC_64bit-Debug/_deps/zppbits-src/zpp_bits.h:2108:17) &>' requested here
zpp_bits.h:2106:20: in instantiation of function template specialization 'zpp::bits::visit_members<(anonymous namespace)::TestStruct &, (lambda at /home/madam/Documents/work/tuw/alpinemaps/build-renderer-Desktop_Qt_6_5_0_GCC_64bit-Debug/_deps/zppbits-src/zpp_bits.h:2108:17)>' requested here
zpp_bits.h:1960:27: in instantiation of function template specialization 'zpp::bits::basic_out<std::vector<std::byte>>::serialize_one<(anonymous namespace)::TestStruct &>' requested here
zpp_bits.h:2401:27: in instantiation of function template specialization 'zpp::bits::basic_out<std::vector<std::byte>>::serialize_many<(anonymous namespace)::TestStruct &>' requested here
test_zppbits.cpp:41:12: in instantiation of function template specialization 'zpp::bits::out<>::operator()<(anonymous namespace)::TestStruct &>' requested here

QByteArray uses a signed size_t (unlike std::vector, which is unsigned). Would be cool if you can add that additional cast.

here is the code:

#include <QByteArray>

#include <zpp_bits.h>

namespace {

struct TestStruct
{
    QByteArray name;
    int age{};
    int padding{};
};
}

int main() {
    std::vector<std::byte> data;
    {
        TestStruct t;
        t.name = "test";
        t.age = 34;
        zpp::bits::out out(data);
        out(t).or_throw();
    }
    {
        TestStruct t;
        zpp::bits::in in(data);
        in(t).or_throw();
        CHECK(t.name == "test");
        CHECK(t.age == 34);
    }
}

thanks, adam

How to serialise large vectors in chunks?

Hi,
we have something like a std::vector. The total size can reach more than a Gb, and we are targeting mobile and web.

atm, we're serialising the vector as a whole into a data vector and then write it to disk (well, only unit tests so far). this works, but we'll have another gigabyte in memory, which is not optimal.

we'd like to serialise the vector in chunks.

a naive way would be something like

    for (const auto& d : m_data) {
        QByteArray data;
        unsigned chunk_size = data.size();
        zpp::bits::out out(data);
        out(chunk_size).or_throw();
        out(m_data).or_throw();
        file.write(data);
    }

and then read back 4 bytes for the chunk size, read the chunk as bytes, deserialise, read the next chunk size etc..

this would mean, that the chunk size is there twice (which is not dramatic, as every chunk has about 100kb). but such code is also error prone (i think). is there a nicer way, or is this the way to go?

if so, would be cool to document that :)
cheers, and thanks again for this amazing piece!

Issues serializing std::chrono::time_point

On Compiler Explorer: https://godbolt.org/z/M8cPzf859

If I outcomment the timepoint member in the thing struct it all works.

Code:

#include <https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h>
#include <chrono>
#include <iostream>

namespace app {
using timepoint = std::chrono::time_point<std::chrono::high_resolution_clock>;
using clock = std::chrono::high_resolution_clock;
}  // namespace app

struct thing {
    int i{};    
    app::timepoint timepoint{app::clock::now()};
};

int main() {
    // setup
    thing t{};
    auto [data, in, out] = zpp::bits::data_in_out();

    // serialize
    t.i = 99;
    auto err_out = out(t);

    // deserialize
    thing t_deser{};
    auto err_in = in(t_deser);

    if (t.i == t_deser.i) {
        std::cout << "OK: Same value " << t.i << " --> " << t_deser.i
                  << std ::endl;
    } else {
        std::cout << "ERROR: Different value" << std ::endl;
    }

    return 0;
}

Some help with unique_ptr, etc.

This time I'm really stumped (sorry for the previous false alarm). I tried the 'members' trick that helped with the previous problem, but I can't get this one to work. I put the example on godbolt, here:

https://godbolt.org/z/Ecbaq53hG

If you could let me know what I'm doing wrong, please. Thanks in advance.

Failed to compile serialization of std::queue

zpp_bits version: 4.4.10

The simplified code:

class SoaBase {
 public:
  constexpr static auto serialize(auto &archive, SoaBase &self) {
    return archive(self.recycled_indexes_);
  }

  std::queue<uint32_t> recycled_indexes_;
};

The error message:

C:\Libraries\zpp_bits\zpp_bits.h(1378): error C2672: 'zpp::bits::access::visit_members': no matching overloaded function found
C:\Libraries\zpp_bits\zpp_bits.h(2524): note: see reference to function template instantiation 'decltype(auto) zpp::bits::visit_members<std::queue<uint32_t,std::deque<uint32_t,std::allocator<std::seed_seq::result_type>>>&,zpp::bits::in<_ReturnType,zpp::bits::options::alloc_limit<1073741824>>::serialize_one::<lambda_1>>(_T0,_T1 &&)' being compiled
        with
        [
            _T0=std::queue<uint32_t,std::deque<uint32_t,std::allocator<std::seed_seq::result_type>>> &,
            _T1=zpp::bits::in<_ReturnType,zpp::bits::options::alloc_limit<1073741824>>::serialize_one::<lambda_1>
        ]
...

Undefined behavior found when serializing zero-sized "item"

Hey, first of all thank you so much for this library! I really enjoyed your talk on youtube about it as well. :)

Today I decided to start using undefined behavior sanitizer for my project and found one hit from zpp_bits, here:
https://github.com/eyalz800/zpp_bits/blob/main/zpp_bits.h#L2621-L2623

It turns out passing nullptr to mempcy is undefined behavior, in my distro's string.h, memcpy is annotated with non_null:

/* Copy N bytes of SRC to DEST.  */
extern void *memcpy (void *__restrict __dest, const void *__restrict __src,

		     size_t __n) __THROW __nonnull ((1, 2));

For completeness sake here's a screenshot from me debugging the issue locally:
ub zpp_bits

I'm guessing the solution is to just not call memcpy if the item_size_in_bytes == 0, but I'm not sure if that means you can just do nothing in that case or what.

SHA265 does not compile for arm32 platform

I'm having issues compiling zpp_bits for my arm32 platform when using SHA256. I can't really find out why this is happening, but I can fix it, by changing all initial values to long unsigned like seen below. But I doubt that this is a portable change.

I'm using gcc13.2 in C++20 mode. Any thoughts how I may fix this in a portable way?

    auto h0 = big_endian{0x6a09e667lu};
    auto h1 = big_endian{0xbb67ae85lu};
    auto h2 = big_endian{0x3c6ef372lu};
    auto h3 = big_endian{0xa54ff53alu};
    auto h4 = big_endian{0x510e527flu};
    auto h5 = big_endian{0x9b05688clu};
    auto h6 = big_endian{0x1f83d9ablu};
    auto h7 = big_endian{0x5be0cd19lu};

    std::array k{big_endian{0x428a2f98lu}, big_endian{0x71374491lu},
                 big_endian{0xb5c0fbcflu}, big_endian{0xe9b5dba5lu},
                 big_endian{0x3956c25blu}, big_endian{0x59f111f1lu},
                 big_endian{0x923f82a4lu}, big_endian{0xab1c5ed5lu},
                 big_endian{0xd807aa98lu}, big_endian{0x12835b01lu},
                 big_endian{0x243185belu}, big_endian{0x550c7dc3lu},
                 big_endian{0x72be5d74lu}, big_endian{0x80deb1felu},
                 big_endian{0x9bdc06a7lu}, big_endian{0xc19bf174lu},
                 big_endian{0xe49b69c1lu}, big_endian{0xefbe4786lu},
                 big_endian{0x0fc19dc6lu}, big_endian{0x240ca1cclu},
                 big_endian{0x2de92c6flu}, big_endian{0x4a7484aalu},
                 big_endian{0x5cb0a9dclu}, big_endian{0x76f988dalu},
                 big_endian{0x983e5152lu}, big_endian{0xa831c66dlu},
                 big_endian{0xb00327c8lu}, big_endian{0xbf597fc7lu},
                 big_endian{0xc6e00bf3lu}, big_endian{0xd5a79147lu},
                 big_endian{0x06ca6351lu}, big_endian{0x14292967lu},
                 big_endian{0x27b70a85lu}, big_endian{0x2e1b2138lu},
                 big_endian{0x4d2c6dfclu}, big_endian{0x53380d13lu},
                 big_endian{0x650a7354lu}, big_endian{0x766a0abblu},
                 big_endian{0x81c2c92elu}, big_endian{0x92722c85lu},
                 big_endian{0xa2bfe8a1lu}, big_endian{0xa81a664blu},
                 big_endian{0xc24b8b70lu}, big_endian{0xc76c51a3lu},
                 big_endian{0xd192e819lu}, big_endian{0xd6990624lu},
                 big_endian{0xf40e3585lu}, big_endian{0x106aa070lu},
                 big_endian{0x19a4c116lu}, big_endian{0x1e376c08lu},
                 big_endian{0x2748774clu}, big_endian{0x34b0bcb5lu},
                 big_endian{0x391c0cb3lu}, big_endian{0x4ed8aa4alu},
                 big_endian{0x5b9cca4flu}, big_endian{0x682e6ff3lu},
                 big_endian{0x748f82eelu}, big_endian{0x78a5636flu},
                 big_endian{0x84c87814lu}, big_endian{0x8cc70208lu},
                 big_endian{0x90befffalu}, big_endian{0xa4506ceblu},
                 big_endian{0xbef9a3f7lu}, big_endian{0xc67178f2lu}};
[build] C:/***/libs/zpp_bits/zpp_bits.h: In instantiation of 'constexpr auto zpp::bits::sha256() [with auto Object = string_literal<char, 21>{std::array<char, 22>{"v1::stored_settings_t"}}; Digest = std::array<std::byte, 32>]':
[build] C:/***/libs/zpp_bits/zpp_bits.h:5556:26:   required from 'constexpr auto zpp::bits::literals::string_literals::operator""_sha256() [with string_literal<...auto...> String = zpp::bits::string_literal<char, 21>{std::array<char, 22>{"v1::stored_settings_t"}}]'
[build] C:/***/***:314:39:   required from here
[build] C:/***/libs/zpp_bits/zpp_bits.h:5461:45: error: no match for 'operator+' (operand types are 'zpp::bits::numbers::big_endian<unsigned int>' and 'std::array<zpp::bits::numbers::big_endian<long unsigned int>, 64>::value_type' {aka 'zpp::bits::numbers::big_endian<long unsigned int>'})
[build]  5461 |             auto temp1 = h + s1 + ch + k[i] + w[i];
[build] C:/***/libs/zpp_bits/zpp_bits.h:5224:27: note: candidate: 'constexpr auto zpp::bits::numbers::operator+(big_endian<unsigned int>, big_endian<unsigned int>)'
[build]  5224 |     constexpr auto friend operator+(big_endian left, big_endian right)
[build]       |                           ^~~~~~~~
[build] C:/***/libs/zpp_bits/zpp_bits.h:5224:65: note:   no known conversion for argument 2 from 'big_endian<long unsigned int>' to 'big_endian<unsigned int>'
[build]  5224 |     constexpr auto friend operator+(big_endian left, big_endian right)
[build]       |                                                      ~~~~~~~~~~~^~~~~
[build] C:/***/libs/zpp_bits/zpp_bits.h:5224:27: note: candidate: 'constexpr auto zpp::bits::numbers::operator+(big_endian<long unsigned int>, big_endian<long unsigned int>)'
[build]  5224 |     constexpr auto friend operator+(big_endian left, big_endian right)
[build]       |                           ^~~~~~~~
[build] C:/***/libs/zpp_bits/zpp_bits.h:5224:48: note:   no known conversion for argument 1 from 'big_endian<unsigned int>' to 'big_endian<long unsigned int>'
[build]  5224 |     constexpr auto friend operator+(big_endian left, big_endian right)
[build]       |                                     ~~~~~~~~~~~^~~~
[build] In file included from C:/***.hpp:16:
[build] C:/***/***:314:57: error: '<expression error>' in namespace 'zpp::bits' does not name a type
[build]   314 |                         using serialize_id = zpp::bits::id<"v1::stored_settings_t"_sha256>;
[build]       |                                                         ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

How to decouple input data and output data in RPC

I have tried to decouple input and output data when using RPC but I can't quite figure out if this is possible. The motivation is: suppose I want to send a large set of data to a server, which will process the data and send back a relatively much smaller response. Currently, it seems that I cannot create a separate output buffer to hold the response data from the RPC call in order to send a minimal amount of data back to the client, I am either forced to use the same size buffer or to reuse the input buffer as data. Could you let me know if this is possible, and perhaps provide an example? Here is a short example of what I'm trying to do:

// in_data is read from client socket
void process_client_request_and_send_to_client(
    std::vector< std::byte >& in_data )
{
    zpp::bits::in in{ in_data };
    // this fails to compile without initial sizing:
    std::vector< std::byte > out_data;
    // this works:
    // std::vector< std::byte > out_data( in_data.size(), std::byte( 'X' ) );
    zpp::bits::out out{ out_data };
    auto server = rpc::server( in, out );
    server.serve().or_throw();
    send_to_client( out_data );
}

Serialize std::optional to protobuf

Hi,

Thank you for developing this impressive library :)

Do you have an example of how to serialize an optional variable? I’m dealing with protobuf files which have a lot of optional fields and would like to use this library instead. However, my code fails to compile when wrapping std::optional around the variables.

#include <string>
#include <iostream>
#include <optional>

#include "zpp_bits.h"

using namespace zpp::bits::literals;

struct person
{
    std::string name;       // = 1
    zpp::bits::vint32_t id; // = 2
    std::string email;      // = 3

    enum phone_type
    {
        mobile = 0,
        home = 1,
        work = 2,
    };

    struct phone_number
    {
        std::string number; // = 1
        phone_type type;    // = 2
    };

    std::vector<phone_number> phones;       // = 4
    std::optional<zpp::bits::vint32_t> tag; // =5
};

struct address_book
{
    std::vector<person> people; // = 1
};

auto serialize(const person &) -> zpp::bits::pb_protocol;
auto serialize(const person::phone_number &) -> zpp::bits::pb_protocol;
auto serialize(const address_book &) -> zpp::bits::pb_protocol;

int main()
{
    auto n = person::phone_number{};
    n.number = "555-4321";
    n.type = person::phone_type::home;

    auto p = person{};
    p.name = "John Doe";
    p.id = 1234;
    p.email = "[email protected]";
    p.tag = {};

    p.phones.push_back(n);

    auto [data, in, out] = data_in_out(zpp::bits::no_size{});

    out(p).or_throw();

    person p_de;
    zpp::bits::in{data, zpp::bits::no_size{}}(p_de).or_throw();

    for (auto val : data)
        std::cout << (char)val;
}

This is the first error I am getting:
/Users/marvin/Coding/serialize-test/zpp_bits.h:234:1036: error: type 'person' decomposes into 5 elements, but only 4 names were provided
Uncommenting the line with the std::optional variable works fine.

Thank you in advance for your suggestions.

How to use RPC in real world?

Hi!
I like your library!
But your RPC framework very strange. How to use it in real world?
There are no any info how to use some transport, for example sockets.
Could you please add some examples?

cannot decompose class type have non-static data members

given we have a base msg structure , how to make it work

struct msg {
    int type;
};

struct person : public msg {
    std::string name;
    int age{};
};

auto [data, in, out] = zpp::bits::data_in_out(zpp::bits::size2b{});

   out(person{1009, "Person1", 25});
    person p;
    in(p);
    

error: cannot decompose class type 'person': both it and its base class 'msg' have non-static data members

Testing at runtime if archive is loading or saving

Hi,

Let's suppose I need to perform additional actions after the class members are loaded. How to determine if the Archive passed to static void serialize(Archive & archive, Self & self) member template method is loading or saving one?

template <typename Archive, typename Self>
static void serialize(Archive & archive, Self & self)
{
     archive(self.width, self.height);

     if (archive.is_loading())
         self.allocate(self.width, self.height);
}

Compilation error with MSVC: "fatal error C1004: unexpected end-of-file found"

A simple project with cmake, created by CLion (version 2022.1.3):

CMakeLists.txt

cmake_minimum_required(VERSION 3.22)
project(test20)

set(CMAKE_CXX_STANDARD 20)

include_directories(C:/Libraries/zpp_bits)

add_executable(test20 main.cpp)

main.cpp

#include <zpp_bits.h>

struct person
{
  std::string name;
  int age{};
};

int main() {
  auto [data, in, out] = zpp::bits::data_in_out();

  out(person{"Person1", 25}, person{"Person2", 35});

  person p1, p2;

  in(p1, p2);

  return 0;
}

Building output:

C:\PROGRA~2\MICROS~1\2019\PROFES~1\VC\Tools\MSVC\1429~1.301\bin\Hostx64\x64\cl.exe  /nologo /TP  -IC:\Libraries\zpp_bits /DWIN32 /D_WINDOWS /EHsc /Zi /Ob0 /Od /RTC1 -MDd -std:c++20 /showIncludes /FoCMakeFiles\test20.dir\main.cpp.obj /FdCMakeFiles\test20.dir\ /FS -c D:\cpp\test20\main.cpp
D:\cpp\test20\main.cpp(12): warning C4834: discarding return value of function with 'nodiscard' attribute
D:\cpp\test20\main.cpp(16): warning C4834: discarding return value of function with 'nodiscard' attribute
C:\Libraries\zpp_bits\zpp_bits.h(865): fatal error C1004: unexpected end-of-file found

Looks like the problem is caused by MSCV (version 16.0), right?
How can I avoid this error?

FR: Deduce options of an achive from type

Hi,
I am trying to use specific configuration of serializer, so I defined type alias for configured version of zpp::bits::out.
Unfortunately, when i want to create instance of serializer i have to pass options one more time.
https://godbolt.org/z/5MqbjETz7
Is it possible to enable an achive to deduce selected options from its type, so that one don't have to pass them in constructor?

seperate load/save function

Is there a facility to have separate save/load methods to serialize/de-serialize values similar to Cereal?

MSVC compile error

  • vs2019 latest
  • c++20
  • code
struct person
{
	std::string name;
	int age{};
};
int main() {
	auto [data, in, out] = zpp::bits::data_in_out();

	// Serialize a few people:
	out(person{ "Person1", 25 }, person{ "Person2", 35 });

	// Define our people.
	person p1, p2;

	// We can now deserialize them either one by one `in(p1)` `in(p2)`, or together, here
	// we chose to do it together in one line:
	in(p1, p2);


	return 0;
}
  • output
Build started...
1>------ Build started: Project: ConsoleApplication1, Configuration: Release Win32 ------
1>ConsoleApplication1.cpp
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(91,65): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(97,82): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(471,54): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(479,50): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(471,54): error C2589: '(': illegal token on right side of '::'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(468,50): message : This diagnostic occurred in the compiler generated function 'size_t zpp::bits::traits::variant_impl<Variant<Types...>>::index(_T0 &&)'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(529): message : see reference to class template instantiation 'zpp::bits::traits::variant_impl<Variant<Types...>>' being compiled
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(658,50): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(658,50): error C2589: '(': illegal token on right side of '::'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(668,50): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(668,50): error C2589: '(': illegal token on right side of '::'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(1140,52): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(1151,52): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(1161,52): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(1173,52): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(1140,52): error C2589: '(': illegal token on right side of '::'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(201,27): message : This diagnostic occurred in the compiler generated function 'auto zpp::bits::access::number_of_members(void)'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(1917,61): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(1917,61): error C2589: '(': illegal token on right side of '::'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(1889,36): message : This diagnostic occurred in the compiler generated function 'zpp::bits::errc zpp::bits::basic_out<ByteView,Options...>::enlarge_for(_T0)'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(2344): message : see reference to class template instantiation 'zpp::bits::basic_out<ByteView,Options...>' being compiled
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(2675,65): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(2675,65): error C2589: '(': illegal token on right side of '::'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(2645,5): message : This diagnostic occurred in the compiler generated function 'zpp::bits::errc zpp::bits::in<ByteView,Options...>::serialize_one(_T0 &&)'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(3050): message : see reference to class template instantiation 'zpp::bits::in<ByteView,Options...>' being compiled
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(4855,62): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(5052,53): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(4855,62): error C2589: '(': illegal token on right side of '::'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(5100): message : see reference to class template instantiation 'zpp::bits::pb<Options...>' being compiled
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(4855,1): error C2059: syntax error: ')'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(97,82): error C2589: '(': illegal token on right side of '::'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(97,82): error C2059: syntax error: ')'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(97,26): error C2947: expecting '>' to terminate template-argument-list, found '>'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(4761,35): error C2976: 'zpp::bits::protocol': too few template arguments
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(99): message : see declaration of 'zpp::bits::protocol'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(4712,5): message : This diagnostic occurred in the compiler generated function 'zpp::bits::errc zpp::bits::pb<Options...>::serialize_one(_T0 &,_T1 &)'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(4860,43): error C2589: '(': illegal token on right side of '::'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(4852,36): message : This diagnostic occurred in the compiler generated function 'zpp::bits::errc zpp::bits::pb<Options...>::operator ()(_T0 &,_T1 &,size_t) const'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(4990,35): error C2976: 'zpp::bits::protocol': too few template arguments
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(99): message : see declaration of 'zpp::bits::protocol'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(4949,43): message : This diagnostic occurred in the compiler generated function 'auto zpp::bits::pb<Options...>::deserialize_field(_T0 &,zpp::bits::pb<Options...>::wire_type,_T1 &)'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(5052,53): error C2589: '(': illegal token on right side of '::'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(4949,43): message : This diagnostic occurred in the compiler generated function 'auto zpp::bits::pb<Options...>::deserialize_field(_T0 &,zpp::bits::pb<Options...>::wire_type,_T1 &)'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(5102,21): error C2976: 'zpp::bits::protocol': too few template arguments
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(99): message : see declaration of 'zpp::bits::protocol'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(5104,67): warning C4003: not enough arguments for function-like macro invocation 'max'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(668): error C2062: type 'unknown-type' unexpected
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(668,1): error C2059: syntax error: ')'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(2440,27): error C2182: 'allocation_limit': illegal use of type 'void'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(2441,1): error C2440: 'initializing': cannot convert from 'void' to 'const int'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(2441,40): message : Expressions of type void cannot be converted to other types
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(1818,27): error C2182: 'allocation_limit': illegal use of type 'void'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(2348): message : see reference to class template instantiation 'zpp::bits::basic_out<ByteView>' being compiled
1>        with
1>        [
1>            ByteView=std::vector<std::byte,std::allocator<std::byte>>
1>        ]
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(3090): message : see reference to class template instantiation 'zpp::bits::out<std::vector<ByteType,std::allocator<ByteType>>>' being compiled
1>        with
1>        [
1>            ByteType=std::byte
1>        ]
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(1818,1): error C2440: 'initializing': cannot convert from 'void' to 'const int'
1>E:\test\zpp_bits-4.4.10\zpp_bits.h(1818,77): message : Expressions of type void cannot be converted to other types
1>C:\Users\user123\source\repos\vs2019\ConsoleApplication1\ConsoleApplication1.cpp(16,5): warning C4834: discarding return value of function with 'nodiscard' attribute
1>C:\Users\user123\source\repos\vs2019\ConsoleApplication1\ConsoleApplication1.cpp(23,4): warning C4834: discarding return value of function with 'nodiscard' attribute
1>Done building project "ConsoleApplication1.vcxproj" -- FAILED.

zpp_bits nukes intellisense

This is likely to be a problem with intellisense, but including zpp_bits.h seems to cause intellisense to run into issues.

Is there any reccomended workaround for this until a day when intellisense is fixed?

Unclear if pImpl is possible given that "using serialize = zpp::bits::members<3>" requires #include <zpp_bits/zpp_bits.h>

Cannot compile using Visual Studio 2019

It seems like Visual Studio has trouble compiling the file. I get a fatal error C1004: unexpected end-of-file found when I include zpp_bits.h.
Also Visual Studio's IntelliSense breaks completely for any file that has #include "zpp_bits.h" for me.

#include "zpp_bits.h"
#include <fstream>
#include <vector>
int main() {
    struct Test {
        using serialize = zpp::bits::members<3>;
        int i = 1, j = 2, k = 3;
    };
    std::vector<Test> tests;
    tests.resize(10);

    std::vector<char> data;
    zpp::bits::out out(data);
    auto result = out(tests);

    std::ofstream save_file("test.txt", std::ios::binary);
    save_file.write(data.data(), data.size());
    save_file.close();

    return 0;
}

When I comment out the line auto result = out(tests); the error disappears and it builds successfully.

I also tried #define ZPP_BITS_INLINE_MODE 0 but that wasn't the problem.

build fails when add constructor

Microsoft Visual Studio Professional 2022 (64-bit) - Current
Version 17.8.1
c++ 20
both mvsc\clang llvm
When I add constructor zpp fails to build.
Are there any limitations for this?

struct test_struct
{
public:
    test_struct() = default; // causes build fail
public:
    std::string field1;
    std::string field2;
};

auto serialize( const test_struct& ) -> zpp::bits::pb_protocol;

int main()
{
    try
    {
        auto [data, out] = zpp::bits::data_out();

        test_struct testix;

        out( testix ).or_throw();

        return 0;
    }
    catch ( const std::exception& error ) 
    {
        std::cout << "Failed with error: " << error.what() << '\n';
        return 1;
    }
    catch ( ... )
    {
        std::cout << "Unknown error\n";
        return 1;
    };
}

error window:

Severity	Code	Description	Project	File	Line	Suppression State	Details
Error	C2607	static assertion failed	zpp_lobby	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h	4515		
Error	C2665	'zpp::bits::failure': no overloaded function could convert all the argument types	zpp_lobby	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h	2312		
Error	C2672	'zpp::bits::access::visit_members': no matching overloaded function found	zpp_lobby	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h	1407		
Error	C2440	'static_assert': cannot convert from 'void' to 'bool'	zpp_lobby	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h	4564		
Error	C2440	'return': cannot convert from 'int' to 'zpp::bits::errc'	zpp_lobby	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h	2314		
Error	C3313	'result': variable cannot have the type 'void'	zpp_lobby	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h	4675		
Error	C3536	'result': cannot be used before it is initialized	zpp_lobby	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h	4685		

output window

1>------ Build started: Project: zpp_lobby, Configuration: Release x64 ------
1>zpp_lobby.cpp
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(4515,35): error C2607: static assertion failed
1>(compiling source file 'zpp_lobby.cpp')
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(4515,35):
1>the template instantiation context (the oldest one first) is
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_lobby.cpp(116,9):
1>	see reference to function template instantiation 'zpp::bits::errc zpp::bits::out<std::vector<std::byte,std::allocator<std::byte>>>::()<test_struct&>(test_struct &)' being compiled
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2400,27):
1>	see reference to function template instantiation 'zpp::bits::errc zpp::bits::basic_out<ByteView>::serialize_many<test_struct&,>(_T0)' being compiled
1>        with
1>        [
1>            ByteView=std::vector<std::byte,std::allocator<std::byte>>,
1>            _T0=test_struct &
1>        ]
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(1960,27):
1>	see reference to function template instantiation 'zpp::bits::errc zpp::bits::basic_out<ByteView>::serialize_one<unsigned int,test_struct&>(_T0)' being compiled
1>        with
1>        [
1>            ByteView=std::vector<std::byte,std::allocator<std::byte>>,
1>            _T0=test_struct &
1>        ]
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,35):
1>	see reference to function template instantiation 'auto zpp::bits::pb<>::()<zpp::bits::basic_out<ByteView>,test_struct>(_T0 &,_T1 &) const' being compiled
1>        with
1>        [
1>            ByteView=std::vector<std::byte,std::allocator<std::byte>>,
1>            _T0=zpp::bits::basic_out<std::vector<std::byte,std::allocator<std::byte>>>,
1>            _T1=test_struct
1>        ]
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(4644,23):
1>	see reference to function template instantiation 'auto zpp::bits::pb<>::check_type<type>(void)' being compiled
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(4564,27):
1>	see reference to function template instantiation 'auto zpp::bits::pb<>::unique_field_numbers<type>(void)' being compiled
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(4564,53): error C2440: 'static_assert': cannot convert from 'void' to 'bool'
1>(compiling source file 'zpp_lobby.cpp')
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(4564,53):
1>Expressions of type void cannot be converted to other types
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(1407,20): error C2672: 'zpp::bits::access::visit_members': no matching overloaded function found
1>(compiling source file 'zpp_lobby.cpp')
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(205,53):
1>could be 'decltype(auto) zpp::bits::access::visit_members(_T0 &&,_T1 &&)'
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(1407,20):
1>	Failed to specialize function template 'decltype(auto) zpp::bits::access::visit_members(_T0 &&,_T1 &&)'
1>		C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(1407,20):
1>		With the following template arguments:
1>			C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(1407,20):
1>			'_T0=_T1 &'
1>			C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(1407,20):
1>			'_T1=_T1 &'
1>		C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(208,74):
1>		the associated constraints are not satisfied
1>			C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(207,37):
1>			the constraint was not satisfied
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(1407,20):
1>the template instantiation context (the oldest one first) is
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(4675,31):
1>	see reference to function template instantiation 'decltype(auto) zpp::bits::visit_members<_T1&,zpp::bits::pb<>::()::<lambda_1>>(_T0,zpp::bits::pb<>::()::<lambda_1> &&)' being compiled
1>        with
1>        [
1>            _T1=test_struct,
1>            _T0=test_struct &
1>        ]
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(4675,29): error C3313: 'result': variable cannot have the type 'void'
1>(compiling source file 'zpp_lobby.cpp')
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(4685,24): error C3536: 'result': cannot be used before it is initialized
1>(compiling source file 'zpp_lobby.cpp')
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,58): error C2665: 'zpp::bits::failure': no overloaded function could convert all the argument types
1>(compiling source file 'zpp_lobby.cpp')
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(160,16):
1>could be 'bool zpp::bits::failure(zpp::bits::errc)'
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,58):
1>	'bool zpp::bits::failure(zpp::bits::errc)': cannot convert argument 1 from 'int' to 'zpp::bits::errc'
1>		C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,66):
1>		'zpp::bits::errc::errc': no overloaded function could convert all the argument types
1>			C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(153,1):
1>			could be 'zpp::bits::errc::errc(zpp::bits::errc &&)'
1>				C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,66):
1>				'zpp::bits::errc::errc(zpp::bits::errc &&)': cannot convert argument 1 from 'int' to 'zpp::bits::errc &&'
1>					C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,66):
1>					Reason: cannot convert from 'int' to 'zpp::bits::errc'
1>					C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,66):
1>					Conversion requires a second user-defined-conversion operator or constructor
1>			C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(153,1):
1>			or       'zpp::bits::errc::errc(const zpp::bits::errc &)'
1>				C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,66):
1>				'zpp::bits::errc::errc(const zpp::bits::errc &)': cannot convert argument 1 from 'int' to 'const zpp::bits::errc &'
1>					C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,66):
1>					Reason: cannot convert from 'int' to 'const zpp::bits::errc'
1>					C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,66):
1>					Conversion requires a second user-defined-conversion operator or constructor
1>			C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(122,5):
1>			or       'zpp::bits::errc::errc(std::errc)'
1>				C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,66):
1>				'zpp::bits::errc::errc(std::errc)': cannot convert argument 1 from 'int' to 'std::errc'
1>					C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,66):
1>					Conversion to enumeration type requires an explicit cast (static_cast, C-style cast or parenthesized function-style cast)
1>			C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,66):
1>			while trying to match the argument list '(int)'
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(115,16):
1>or       'bool zpp::bits::failure(std::errc)'
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,58):
1>	'bool zpp::bits::failure(std::errc)': cannot convert argument 1 from 'int' to 'std::errc'
1>		C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,66):
1>		Conversion to enumeration type requires an explicit cast (static_cast, C-style cast or parenthesized function-style cast)
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2312,58):
1>while trying to match the argument list '(int)'
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2314,21): error C2440: 'return': cannot convert from 'int' to 'zpp::bits::errc'
1>(compiling source file 'zpp_lobby.cpp')
1>C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2314,21):
1>'zpp::bits::errc::errc': no overloaded function could convert all the argument types
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(153,1):
1>	could be 'zpp::bits::errc::errc(zpp::bits::errc &&)'
1>		C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2314,21):
1>		'zpp::bits::errc::errc(zpp::bits::errc &&)': cannot convert argument 1 from 'int' to 'zpp::bits::errc &&'
1>			C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2314,28):
1>			Reason: cannot convert from 'int' to 'zpp::bits::errc'
1>			C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2314,28):
1>			Conversion requires a second user-defined-conversion operator or constructor
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(153,1):
1>	or       'zpp::bits::errc::errc(const zpp::bits::errc &)'
1>		C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2314,21):
1>		'zpp::bits::errc::errc(const zpp::bits::errc &)': cannot convert argument 1 from 'int' to 'const zpp::bits::errc &'
1>			C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2314,28):
1>			Reason: cannot convert from 'int' to 'const zpp::bits::errc'
1>			C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2314,28):
1>			Conversion requires a second user-defined-conversion operator or constructor
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(122,5):
1>	or       'zpp::bits::errc::errc(std::errc)'
1>		C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2314,21):
1>		'zpp::bits::errc::errc(std::errc)': cannot convert argument 1 from 'int' to 'std::errc'
1>			C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2314,28):
1>			Conversion to enumeration type requires an explicit cast (static_cast, C-style cast or parenthesized function-style cast)
1>	C:\Users\koval\source\repos\zpp_lobby\zpp_bits_s.h(2314,21):
1>	while trying to match the argument list '(int)'

FR: Support for bitfields?

We have some types to serialise, and we'd ideally like to use bitfields - as this matches how we really want to use the data, so prevents other sorts of errors from trying to pack values in and risking getting this wrong.

If I try to serialise a type containing a bitfield, I get back errors about taking non-const references to a bitfield, which is illegal given that a reference or pointer to the encasing type (e.g. a 32-bit word) won't point to a real subobject of that type, and so trying to mutate it would result in undefined behaviour.

Do you reckon there is any chance that the library would be able to support types with bitfields in some manner? Or would that require such an amount of reworking that goes against the current design that it's out of scope?

Thanks!

Fail to (de)serialize struct containing an std::optional

https://godbolt.org/z/4Gavhvbbo

#include "https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h"

#include <optional>

struct MyStruct
{
    std::optional<int> a;
};

auto deserialize(std::vector<std::byte> data, MyStruct& output)
{
    zpp::bits::in in{data};
    return in(output);
}

auto serialize(std::vector<std::byte>& data, const MyStruct& input)
{
    zpp::bits::out out{data};
    return out(input);
}

Both deserialize and serialize fail to compile.

compile error is not helpful

https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h: In instantiation of 'constexpr decltype(auto) zpp::bits::visit_members(auto:118&&, auto:119&&) [with auto:118 = MyStruct&; auto:119 = in<std::vector<std::byte> >::serialize_one<MyStruct&>(MyStruct&)::<lambda(auto:162&& ...)>]':
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:2617:33:   required from 'constexpr zpp::bits::errc zpp::bits::in<ByteView, Options>::serialize_one(auto:150&&) [with auto:150 = MyStruct&; ByteView = std::vector<std::byte>; Options = {}]'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:2511:40:   required from 'constexpr zpp::bits::errc zpp::bits::in<ByteView, Options>::serialize_many(auto:148&&, auto:149&& ...) [with auto:148 = MyStruct&; auto:149 = {}; ByteView = std::vector<std::byte>; Options = {}]'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:2457:30:   required from 'constexpr auto zpp::bits::in<ByteView, Options>::operator()(auto:147&& ...) [with auto:147 = {MyStruct&}; ByteView = std::vector<std::byte>; Options = {}]'
<source>:13:14:   required from here
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:1381:33: error: no matching function for call to 'zpp::bits::access::visit_members(MyStruct&, zpp::bits::in<std::vector<std::byte> >::serialize_one<MyStruct&>(MyStruct&)::<lambda(auto:162&& ...)>&)'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:205:53: note: candidate: 'template<class auto:50, class auto:51> static constexpr decltype(auto) zpp::bits::access::visit_members(auto:50&&, auto:51&&) requires 0 <= number_of_members<decltype(zpp::bits::access::visit_members::object)>() && number_of_members<decltype(zpp::bits::access::visit_members::object)>() <= zpp::bits::access::max_visit_members'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:205:53: note:   template argument deduction/substitution failed:
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:205:53: note: constraints not satisfied
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h: In substitution of 'template<class auto:50, class auto:51> static constexpr decltype(auto) zpp::bits::access::visit_members(auto:50&&, auto:51&&) requires 0 <= number_of_members<decltype(object)>() && number_of_members<decltype(object)>() <= zpp::bits::access::max_visit_members [with auto:50 = MyStruct&; auto:51 = zpp::bits::in<std::vector<std::byte> >::serialize_one<MyStruct&>(MyStruct&)::<lambda(auto:162&& ...)>&]':
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:1381:33:   required from 'constexpr decltype(auto) zpp::bits::visit_members(auto:118&&, auto:119&&) [with auto:118 = MyStruct&; auto:119 = in<std::vector<std::byte> >::serialize_one<MyStruct&>(MyStruct&)::<lambda(auto:162&& ...)>]'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:2617:33:   required from 'constexpr zpp::bits::errc zpp::bits::in<ByteView, Options>::serialize_one(auto:150&&) [with auto:150 = MyStruct&; ByteView = std::vector<std::byte>; Options = {}]'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:2511:40:   required from 'constexpr zpp::bits::errc zpp::bits::in<ByteView, Options>::serialize_many(auto:148&&, auto:149&& ...) [with auto:148 = MyStruct&; auto:149 = {}; ByteView = std::vector<std::byte>; Options = {}]'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:2457:30:   required from 'constexpr auto zpp::bits::in<ByteView, Options>::operator()(auto:147&& ...) [with auto:147 = {MyStruct&}; ByteView = std::vector<std::byte>; Options = {}]'
<source>:13:14:   required from here
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:205:53:   required by the constraints of 'template<class auto:50, class auto:51> static constexpr decltype(auto) zpp::bits::access::visit_members(auto:50&&, auto:51&&) requires 0 <= number_of_members<decltype(object)>() && number_of_members<decltype(object)>() <= zpp::bits::access::max_visit_members'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:207:37: note: the expression '0 <= number_of_members<decltype(object)>() [with auto:51 = zpp::bits::in<std::vector<std::byte, std::allocator<std::byte> > >::serialize_one::._anon_219&; auto:50 = MyStruct&]' evaluated to 'false'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h: In instantiation of 'constexpr decltype(auto) zpp::bits::visit_members(auto:118&&, auto:119&&) [with auto:118 = const MyStruct&; auto:119 = basic_out<std::vector<std::byte> >::serialize_one<const MyStruct&>(const MyStruct&)::<lambda(auto:144&& ...)>]':
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:2076:33:   required from 'constexpr zpp::bits::errc zpp::bits::basic_out<ByteView, Options>::serialize_one(auto:133&&) [with auto:133 = const MyStruct&; ByteView = std::vector<std::byte>; Options = {}]'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:1934:40:   required from 'constexpr zpp::bits::errc zpp::bits::basic_out<ByteView, Options>::serialize_many(auto:131&&, auto:132&& ...) [with auto:131 = const MyStruct&; auto:132 = {}; ByteView = std::vector<std::byte>; Options = {}]'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:2371:41:   required from 'constexpr auto zpp::bits::out<ByteView, Options>::operator()(auto:146&& ...) [with auto:146 = {const MyStruct&}; ByteView = std::vector<std::byte>; Options = {}]'
<source>:19:15:   required from here
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:1381:33: error: no matching function for call to 'zpp::bits::access::visit_members(const MyStruct&, zpp::bits::basic_out<std::vector<std::byte> >::serialize_one<const MyStruct&>(const MyStruct&)::<lambda(auto:144&& ...)>&)'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:205:53: note: candidate: 'template<class auto:50, class auto:51> static constexpr decltype(auto) zpp::bits::access::visit_members(auto:50&&, auto:51&&) requires 0 <= number_of_members<decltype(zpp::bits::access::visit_members::object)>() && number_of_members<decltype(zpp::bits::access::visit_members::object)>() <= zpp::bits::access::max_visit_members'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:205:53: note:   template argument deduction/substitution failed:
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:205:53: note: constraints not satisfied
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h: In substitution of 'template<class auto:50, class auto:51> static constexpr decltype(auto) zpp::bits::access::visit_members(auto:50&&, auto:51&&) requires 0 <= number_of_members<decltype(object)>() && number_of_members<decltype(object)>() <= zpp::bits::access::max_visit_members [with auto:50 = const MyStruct&; auto:51 = zpp::bits::basic_out<std::vector<std::byte> >::serialize_one<const MyStruct&>(const MyStruct&)::<lambda(auto:144&& ...)>&]':
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:1381:33:   required from 'constexpr decltype(auto) zpp::bits::visit_members(auto:118&&, auto:119&&) [with auto:118 = const MyStruct&; auto:119 = basic_out<std::vector<std::byte> >::serialize_one<const MyStruct&>(const MyStruct&)::<lambda(auto:144&& ...)>]'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:2076:33:   required from 'constexpr zpp::bits::errc zpp::bits::basic_out<ByteView, Options>::serialize_one(auto:133&&) [with auto:133 = const MyStruct&; ByteView = std::vector<std::byte>; Options = {}]'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:1934:40:   required from 'constexpr zpp::bits::errc zpp::bits::basic_out<ByteView, Options>::serialize_many(auto:131&&, auto:132&& ...) [with auto:131 = const MyStruct&; auto:132 = {}; ByteView = std::vector<std::byte>; Options = {}]'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:2371:41:   required from 'constexpr auto zpp::bits::out<ByteView, Options>::operator()(auto:146&& ...) [with auto:146 = {const MyStruct&}; ByteView = std::vector<std::byte>; Options = {}]'
<source>:19:15:   required from here
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:205:53:   required by the constraints of 'template<class auto:50, class auto:51> static constexpr decltype(auto) zpp::bits::access::visit_members(auto:50&&, auto:51&&) requires 0 <= number_of_members<decltype(object)>() && number_of_members<decltype(object)>() <= zpp::bits::access::max_visit_members'
https://raw.githubusercontent.com/eyalz800/zpp_bits/main/zpp_bits.h:207:37: note: the expression '0 <= number_of_members<decltype(object)>() [with auto:51 = zpp::bits::basic_out<std::vector<std::byte, std::allocator<std::byte> > >::serialize_one::._anon_220&; auto:50 = const MyStruct&]' evaluated to 'false'
Compiler returned: 1

problem with a recursive structure

Hi, I define a struct which represents a tree.

struct Node {
    std::vector<std::pair<uint32_t, std::shared_ptr<Node>>> children;
    std::vector<uint32_t> word;
};

But gcc reports an error.

zpp_bits.h:1817:20: error: function 'serialize_many<std::shared_ptr<Node> &>' with deduced return type cannot be used before it is defined
return serialize_many(items...);

Does it mean that recursion is not yet supported or something like that?

Serializing to/from file.

Hello!
Is there an established way of serializing directly to/from a file? Or one just would implement data with push_back and/or append methods and pass to in/out?
Thanks in advance!

Compiling with MSVC results in a "fatal error C1001: Internal compiler error"

When I compile the following code with Visual Studio 2022 Preview 5, I get an error

template <typename Container>
  static auto parse(Container&& buf, dos_header& header) -> std::error_code {
    if (buf.size() < size) {
      return make_error_code(deep::parser::parse_error::too_small_buffer);
    }

    auto in = zpp::bits::input(buf); // this is the line that causes the error (probably)
    in(header);

    if (header.e_magic != magic) {
      return deep::parser::make_error_code(
          deep::parser::parse_error::invalid_magic_number);
    }

    return {};
  }

The error I get:
fatal error C1001: Internal compiler error. (compiler file 'D:\a\_work\1\s\src\vctools\Compiler\CxxFE\sl\p1\c\module\parse-tree-resolve.cpp', line 736) To work around this problem, try simplifying or changing the program near the locations listed above.

Can the function signature of explicit serialization not use `auto`?

struct person
{
    constexpr static auto serialize(auto & archive, person & self)
    {
        return archive(self.name, self.age);
    }

    std::string name;
    int age{};
};

Will this cause const person & self not to be matched?

I want to write this way, because it enables the following member variables to be parsed by the IDE, so that I can quickly find that the member variables are written incorrectly, or have been deleted.

C style arrays

Hi,
I experience similar problem as in this issue: #111
When i trying to serialize c style array.

support for std::shared_ptr<const T>

Hi, would it be possible to enable this pattern and generally containers of const ?
cereal or boost accept it, it is quite useful in come situations .

I can overcome the problem with changing line 3316 (ugly)
auto loaded = access::make_unique<std::remove_const_t>();;

However a question remains: serialization of polymorphic types does not seems to work (build back a shared_ptr from shared_ptr)

can't serialize empty tuple

First this is a great library.

Issue:
Serializing empty tuple result in compile time error. It would be nice if it didn't like it doesn't for e.g. empty vector.

std::tuple<> t;
auto [m,out] = zpp::bits::data_out();
out(t);

self_referencing behaves differently for structs and classes

Hi!

I was checking the test example in https://github.com/eyalz800/zpp_bits/blob/main/test/src/test_graph.cpp, and by tying to replicate it I found some inconsistent behaviour, but I'm not sure if it is on purpose or an oversight:

Let's define a node and a voxel. One is a struct (as in your example) but the other one is a class:

struct node
{
    int value;
    std::vector<node> nodes;
};

class voxel
{
    friend zpp::bits::access;

private:
    int value;
    std::vector<voxel> vox;
};

static_assert(zpp::bits::concepts::self_referencing<node>); // this is OK
static_assert(zpp::bits::concepts::self_referencing<voxel>); // this raises error C2607: static assertion failed`

" note: the concept 'zpp::bits::concepts::self_referencing' evaluated to false"

Thanks!

Raspberry Pi - GCC Complication Error

pi@raspberrypi:~/C++/RaftConsensusProtocol $ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/aarch64-linux-gnu/10/lto-wrapper
Target: aarch64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Debian 10.2.1-6' --with-bugurl=file:///usr/share/doc/gcc-10/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-10 --program-prefix=aarch64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-libquadmath --disable-libquadmath-support --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --enable-fix-cortex-a53-843419 --disable-werror --enable-checking=release --build=aarch64-linux-gnu --host=aarch64-linux-gnu --target=aarch64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-mutex
Thread model: posix
Supported LTO compression algorithms: zlib zstd
gcc version 10.2.1 20210110 (Debian 10.2.1-6)

CC = ccache g++
CFLAGS = -std=c++2a -fcoroutines -Wall -march=native -O3 -I../Dependencies/zpp_bits
LDFLAGS = -pthread -lssl -lcrypto -ldl -l:libboost_json.a -l:libboost_url.a
EXECUTABLE = RaftConsensusProtocol
OBJECTS = Application.o

all: $(EXECUTABLE)
clean:
rm -rf $(EXECUTABLE) $(OBJECTS)
$(EXECUTABLE): $(OBJECTS)
$(CC) $(OBJECTS) -o $(EXECUTABLE) $(LDFLAGS)
%.o: %.cpp
$(CC) $(CFLAGS) -c $&lt; -o $@

pi@raspberrypi:~/C++/RaftConsensusProtocol $ make clean && time make && ./RaftConsensusProtocol
rm -rf RaftConsensusProtocol Application.o
ccache g++ -std=gnu++2a -fcoroutines -Wall -march=native -O3 -I../Dependencies/zpp_bits -c Application.cpp -o Application.o
In file included from Networking/PCH.hpp:34,
from Application.cpp:14:
../Dependencies/zpp_bits/zpp_bits.h: In function ‘constexpr ToType zpp::bits::std::bit_cast(const FromType&)’:
../Dependencies/zpp_bits/zpp_bits.h:80:37: error: expected primary-expression before ‘,’ token
80 | return __builtin_bit_cast(ToType, from);
| ^
../Dependencies/zpp_bits/zpp_bits.h: In instantiation of ‘constexpr ToType zpp::bits::std::bit_cast(const FromType&) [with ToType = std::array<unsigned char, 4>; FromType = zpp::bits::numbers::big_endian; = void]’:
../Dependencies/zpp_bits/zpp_bits.h:5141:80: required from ‘constexpr auto zpp::bits::numbers::operator+(zpp::bits::numbers::big_endian, zpp::bits::numbers::big_endian)’
../Dependencies/zpp_bits/zpp_bits.h:5164:25: required from ‘constexpr auto& zpp::bits::numbers::big_endian::operator+=(zpp::bits::numbers::big_endian) [with Type = unsigned int]’
../Dependencies/zpp_bits/zpp_bits.h:5266:15: required from here
../Dependencies/zpp_bits/zpp_bits.h:80:30: error: ‘__builtin_bit_cast’ was not declared in this scope; did you mean ‘__builtin_strcat’?
80 | return __builtin_bit_cast(ToType, from);
| ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~
| __builtin_strcat
../Dependencies/zpp_bits/zpp_bits.h: In instantiation of ‘constexpr ToType zpp::bits::std::bit_cast(const FromType&) [with ToType = zpp::bits::numbers::big_endian; FromType = std::array<unsigned char, 4>; = void]’:
../Dependencies/zpp_bits/zpp_bits.h:5154:41: required from ‘constexpr auto zpp::bits::numbers::operator+(zpp::bits::numbers::big_endian, zpp::bits::numbers::big_endian)’
../Dependencies/zpp_bits/zpp_bits.h:5164:25: required from ‘constexpr auto& zpp::bits::numbers::big_endian::operator+=(zpp::bits::numbers::big_endian) [with Type = unsigned int]’
../Dependencies/zpp_bits/zpp_bits.h:5266:15: required from here
../Dependencies/zpp_bits/zpp_bits.h:80:30: error: ‘__builtin_bit_cast’ was not declared in this scope; did you mean ‘__builtin_strcat’?
80 | return __builtin_bit_cast(ToType, from);
| ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~
| __builtin_strcat
../Dependencies/zpp_bits/zpp_bits.h: In instantiation of ‘constexpr ToType zpp::bits::std::bit_cast(const FromType&) [with ToType = zpp::bits::numbers::big_endian; FromType = std::array<std::byte, 4>; = void]’:
../Dependencies/zpp_bits/zpp_bits.h:1217:64: required from ‘static constexpr auto zpp::bits::access::endian_independent_byte_serializable() [with Type = zpp::bits::numbers::big_endian]’
../Dependencies/zpp_bits/zpp_bits.h:820:55: required from ‘constexpr zpp::bits::errc zpp::bits::basic_out<ByteView, Options>::serialize_one(auto:133&&) [with auto:133 = zpp::bits::numbers::big_endian&; ByteView = std::array<std::byte, 20>; Options = {}]’
../Dependencies/zpp_bits/zpp_bits.h:1902:40: required from ‘constexpr zpp::bits::errc zpp::bits::basic_out<ByteView, Options>::serialize_many(auto:131&&, auto:132&& ...) [with auto:131 = zpp::bits::numbers::big_endian&; auto:132 = {zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&}; ByteView = std::array<std::byte, 20>; Options = {}]’
../Dependencies/zpp_bits/zpp_bits.h:2319:34: required from ‘constexpr auto zpp::bits::out<ByteView, Options>::operator()(auto:146&& ...) [with auto:146 = {zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&}; ByteView = std::array<std::byte, 20>; Options = {}]’
../Dependencies/zpp_bits/zpp_bits.h:5274:40: required from here
../Dependencies/zpp_bits/zpp_bits.h:80:30: error: ‘__builtin_bit_cast’ was not declared in this scope; did you mean ‘__builtin_strcat’?
80 | return __builtin_bit_cast(ToType, from);
| ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~
| __builtin_strcat
../Dependencies/zpp_bits/zpp_bits.h: In instantiation of ‘constexpr ToType zpp::bits::std::bit_cast(const FromType&) [with ToType = std::array<std::byte, 4>; FromType = unsigned int; = void]’:
../Dependencies/zpp_bits/zpp_bits.h:1959:46: required from ‘constexpr zpp::bits::errc zpp::bits::basic_out<ByteView, Options>::serialize_one(auto:133&&) [with auto:133 = unsigned int&; ByteView = std::array<std::byte, 20>; Options = {}]’
../Dependencies/zpp_bits/zpp_bits.h:1902:40: required from ‘constexpr zpp::bits::errc zpp::bits::basic_out<ByteView, Options>::serialize_many(auto:131&&, auto:132&& ...) [with auto:131 = unsigned int&; auto:132 = {}; ByteView = std::array<std::byte, 20>; Options = {}]’
../Dependencies/zpp_bits/zpp_bits.h:2047:51: required from ‘zpp::bits::basic_out<ByteView, Options>::serialize_one<zpp::bits::numbers::big_endian&>::<lambda(auto:144&& ...)> [with auto:144 = {unsigned int&}]’
../Dependencies/zpp_bits/zpp_bits.h:215:127: required from ‘static constexpr decltype(auto) zpp::bits::access::visit_members(auto:50&&, auto:51&&) requires 0 <= number_of_members<decltype(zpp::bits::access::visit_members::object)>() && number_of_members<decltype(zpp::bits::access::visit_members::object)>() <= zpp::bits::access::max_visit_members [with auto:50 = zpp::bits::numbers::big_endian&; auto:51 = zpp::bits::basic_out<ByteView, Options>::serialize_one<zpp::bits::numbers::big_endian&>::<lambda(auto:144&& ...)>&]’
../Dependencies/zpp_bits/zpp_bits.h:1349:33: required from ‘constexpr decltype(auto) zpp::bits::visit_members(auto:118&&, auto:119&&) [with auto:118 = zpp::bits::numbers::big_endian&; auto:119 = zpp::bits::basic_out<ByteView, Options>::serialize_one<zpp::bits::numbers::big_endian&>::<lambda(auto:144&& ...)>]’
../Dependencies/zpp_bits/zpp_bits.h:2044:33: required from ‘constexpr zpp::bits::errc zpp::bits::basic_out<ByteView, Options>::serialize_one(auto:133&&) [with auto:133 = zpp::bits::numbers::big_endian&; ByteView = std::array<std::byte, 20>; Options = {}]’
../Dependencies/zpp_bits/zpp_bits.h:1902:40: required from ‘constexpr zpp::bits::errc zpp::bits::basic_out<ByteView, Options>::serialize_many(auto:131&&, auto:132&& ...) [with auto:131 = zpp::bits::numbers::big_endian&; auto:132 = {zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&}; ByteView = std::array<std::byte, 20>; Options = {}]’
../Dependencies/zpp_bits/zpp_bits.h:2319:34: required from ‘constexpr auto zpp::bits::out<ByteView, Options>::operator()(auto:146&& ...) [with auto:146 = {zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&, zpp::bits::numbers::big_endian&}; ByteView = std::array<std::byte, 20>; Options = {}]’
../Dependencies/zpp_bits/zpp_bits.h:5274:40: required from here
../Dependencies/zpp_bits/zpp_bits.h:80:30: error: ‘__builtin_bit_cast’ was not declared in this scope; did you mean ‘__builtin_strcat’?
80 | return __builtin_bit_cast(ToType, from);
| ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~
| __builtin_strcat

https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html

Built-in Function: type __builtin_bit_cast (type, arg)
The __builtin_bit_cast function is available only in C++. The built-in is intended to be used by implementations of the std::bit_cast C++ template function. Programs should make use of the latter function rather than invoking the built-in directly.

This built-in function allows reinterpreting the bits of the arg argument as if it had type type. type and the type of the arg argument need to be trivially copyable types with the same size. When manifestly constant-evaluated, it performs extra diagnostics required for std::bit_cast and returns a constant expression if arg is a constant expression. For more details refer to the latest revision of the C++ standard.

Please document serialisation format

We are using an in-house serialisation library with many of the same goals as this library. Ours is based on the concepts of Apache Avro, which is also overhead-free given that the schema is supplied separately - in the case of C++ code, the source code can serve as the schema. For maintainability reasons however, we could consider to switch to yours instead at some point. But in order to evaluate if the format is indeed ticking our boxes, it would be great to have at least a high-level description of the serialisation format. From our perspective, there is no need for a full "spec", we're willing to read source-code if we need to know the details.

Serialization size estimate?

My usage senario is:

  • estimize the serialization size of multple objects
  • allocate a large enough buffer
  • serialize those objects in sequence

Can zpp_bits add GetSizeInBytes() or something else to estimize the size of serialization of an object?

Multi-language support

Hi there,

I'm building an IoT system that requires serialization, have read a lot about the different schemas and libraries, finally concluded to use protobuf by using protozero library and built my serializers and parsers around.

As I'm in the early stages of building the system, it's the best time to change the schema if a more suitable one was found, and just watched your talk at CppCon20 which brought zpp::bits back to my considerations.

The tendency of eliminating zpp::bits was the lack of support for other languages. As the IoT system has several entities (Server, Device, Mobile) with apparently different development languages, here becomes a basic intuitive question: how to support other entities development, not letting the developers lost?

I'd appreciate if you can guide/point me to solutions fulfills that concern.

Best Regards,
Hamza Hajeir


Notes:

  • One can take a look about what criteria I was looking for at this question
  • It perhaps be a good idea to support parsing to data views (as std::span or std::string_view) for wire types as bytes and string (as described in protobuf), which will enhance deserialization time.

Crash when moving postion and writing data

The following will crash:
(use case: write data before a header containing the actual data length)

std::vector<std::uint8_t> sData;
zpp::bits::out out(sData);

out.position() += headerSize;
out(dataObject).or_throw(); // will fail - and crash

I created a PR with changes that fix this issue and work for my current Windows and Linux project:
#132

std::array treated as agregate

Hi,

I am trying to serialize POD structure that has std::array field, unfortunately it doesn't works.
At first glance, the library tries to unpack std::array, as it deals with aggregates.
Is it expected bahaviour?

Following link displays code example:
https://godbolt.org/z/6fGEda1ja

MSVC Visual Studio 16 2019 compile error

Hi and thank you for publishing this very nice library. I tried to switch from zpp serializer to zpp::bits with a cross platform C++20 project, and I got a weird compiler error with my github actions with Visual Studio 16 2019. (I have not yet tried with newer MSVC compiler versions)

zpp_bits.h(885,1): fatal error C1004: unexpected end-of-file found

Do you have any idea how to get Visual Studio 16 2019 to compile zpp_bits?

Github action: relevant OS and compiler config: (gcc, and clang works)

          - os: windows-2019
            generator: "Visual Studio 16 2019"
            compiler: msvc
            build_type: Release```

Detected compiler (CMake):

-- The CXX compiler identification is MSVC 19.29.30147.0
-- The C compiler identification is MSVC 19.29.30147.0

Error message:

...
  tcp-connection.cc
D:/a/server/server/third-party/zpp_bits\zpp_bits/zpp_bits.h(885,1): fatal error C1004: unexpected end-of-file found [D:\a\server\server\build\utils\tcp\utils_tcp.vcxproj]

Add support of mixins/user-data for archives

My use case involves serializing many small objects, so I'd like to have a single global std::size_t input_version as a member of zpp::bits::in archive to be able to access it in all auto serialize(auto& archive, auto& self) functions.

At the moment I've made it work with inheritance and a cast like this:

constexpr static auto serialize(auto& archive, auto& self)
{
    using archive_type = std::remove_cvref_t<decltype(archive)>;

    if constexpr (archive_type::kind() == zpp::bits::kind::in)
    {
        auto& archive_ver = static_cast<zpp::bits::in_versioned<const std::vector<std::byte>>&>(archive);

        zpp::bits::errc e;
        switch (archive_ver.input_version)
        // etc.

That was cumbersome to do because I also needed to reimplement helper functions like data_in_ver_out, input etc.
I also realize that my use case is pretty niche and other users might want different functionality. Therefore, I propose extending in and out classes as such:

template <concepts::byte_view ByteView = std::vector<std::byte>,
          typename... Options,
          typename... Extensions>
class in : public Extensions...

If you'd like, I'm willing to implement this or a different suggested design.

Thanks for an outstanding library.

Cannot serialize optional unique pointer

I read the documentation which indicated that optional<unique_ptr> was supported by zpp bits. Not sure if I'm doing something very stupid, but using gcc-13 on Mac, I cannot get the following code to compile.

#include "zpp_bits.h"
#include <memory>
#include <string>
#include <vector>

namespace zzz
{
template < class T, class B >
std::size_t serialize( const T& t, B& data )
{
    zpp::bits::out out{ data };
    if ( auto result = out( t ); failure( result ) ) { return 0; }
    return out.position();
}

template < class T, class B >
std::size_t deserialize( T& t, const B& data )
{
    zpp::bits::in in{ data };
    if ( auto result = in( t ); failure( result ) ) { return 0; }
    return in.position();
}

}

struct foo
{
    std::string name;
    int         count;
};

struct bar
{
    std::optional< std::unique_ptr< foo > > foos;
};

int main()
{
    {
        std::vector< char >                     data;
        std::optional< std::unique_ptr< foo > > foos;
        // compiles ok:
        zzz::serialize( foos, data );
    }

    {
        std::vector< char > data;
        // does not compile:
        bar b;
        zzz::serialize( b, data );
    }
}

Replace std::aligned_storage due to depreceation

Hello,

According to P1413R3, aligned_storage (and aligned_union) are not to be used. From the paper's background:

aligned_* are harmful to codebases and should not be used. At a high level:

Using aligned_* invokes undefined behavior (The types cannot provide storage.)
The guarantees are incorrect (The standard only requires that the type be at least as large as requested but does not put an upper bound on the size.)
The API is wrong for a plethora of reasons (See "On the API".) Because the API is wrong, almost all usage involves the same repeated pre-work (See "Existing usage".)

Also see this stack overflow discussion for a brief summary of the issue, and cppreference for this paper being a part of the C++23 standard.

The suggested replacement (from the linked document) is:

template <typename T>
using aligned_storage = alignas(T) std::byte[sizeof(T)];

Aligned storage is used ~7 times in the codebase, and I would be happy to submit a PR with the proposed changes.

What about CMake?

Would you like to add CMake support? It's not so hard for header-only library
You can use this tool for making cmake project just in a few commands

How to implement serialization function of derived class?

class MyDerived: public MyBase {
  constexpr static auto serialize(auto &archive, auto &self) {
    MyBase::serialize(archive, self);
    return archive(self.member1, self.member2);
  }
}

Is this code correct?
Will it cause some errors not to be handled?
Maybe we should add a piece of document on how to serialize derived class gracefully?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.