一种现代 C++20 二进制序列化和 RPC 库,只有一个头文件。
该库是 zpp::serializer 的后继者。该库试图变得更简单,但其 API 与其前身或多或少相似。
constexpr
zpp::bits
比zpp::serializer
更容易键入。zpp::serializer
全局多态类型相比,基于变体和灵活的序列化 id 的现代化多态性。对于许多类型,启用序列化是透明的,不需要额外的代码行。这些类型必须是聚合类型,且具有非数组成员。下面是一个包含姓名和年龄的person
类示例:
struct person
{
std::string name;
int age{};
};
如何将人序列化到字节向量或从字节向量序列化的示例:
// 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);
这个例子几乎可以工作,我们被警告说我们正在丢弃返回值。对于错误检查,请继续阅读。
我们需要检查错误,该库提供了多种方法来执行此操作 - 基于返回值、基于异常或基于 zpp:: throwing。
基于返回值的方式是最明确的,或者如果您只是更喜欢返回值:
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.
}
使用.or_throw()
基于异常的方式(将其读作“成功或抛出” - 因此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 ;
});
}
另一个选项是 zpp::throwing,其中错误检查变成两个简单的co_await
,为了了解如何检查错误,我们提供了完整的 main 函数:
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 ;
});
}
上述所有方法都在内部使用以下错误代码,并且可以使用基于返回值的比较运算符进行检查,或者通过检查std::system_error
或zpp::throwing
的内部错误代码(具体取决于您使用的方法):
std::errc::result_out_of_range
- 尝试从太短的缓冲区写入或读取。std::errc::no_buffer_space
- 不断增长的缓冲区将超出分配限制或溢出。std::errc::value_too_large
- varint(可变长度整数)编码超出了表示限制。std::errc::message_size
- 消息大小超出用户定义的分配限制。std::errc::not_supported
- 尝试调用未列为受支持的 RPC。std::errc::bad_message
- 尝试读取无法识别类型的变体。std::errc::invalid_argument
- 尝试序列化空指针或无值变体。std::errc::protocol_error
- 尝试反序列化无效的协议消息。 对于大多数非聚合类型(或具有数组成员的聚合类型),启用序列化是一件简单的事情。以下是非聚合person
类的示例:
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{};
};
大多数时候,我们序列化的类型可以使用结构化绑定,并且该库利用了这一点,但是您需要提供类中的成员数量,以便使用上面的方法进行工作。
这也适用于参数相关的查找,允许不修改源类:
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
在某些编译器中, SFINAE在if constexpr
和unevaluated lambda expression
下使用requires expression
。这意味着即使使用非聚合类型,如果所有成员都位于同一结构中,也可以自动检测成员数量。要选择加入,请定义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{};
};
这适用于clang 13
,但是其可移植性尚不清楚,因为在gcc
中它不起作用(这是一个硬错误),并且它在标准中明确指出在类似情况下不允许SFINAE ,所以它默认情况下处于关闭状态。
如果您的数据成员或默认构造函数是私有的,您需要与zpp::bits::access
成为朋友,如下所示:
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{};
};
要使用显式序列化启用任何对象的保存和加载(无论结构化绑定兼容性如何,该序列化都有效),请将以下行添加到您的类中:
constexpr static auto serialize ( auto & archive, auto & self)
{
return archive (self. object_1 , self. object_2 , ...);
}
请注意, object_1, object_2, ...
是类的非静态数据成员。
下面是一个具有显式序列化函数的 person 类的示例:
struct person
{
constexpr static auto serialize ( auto & archive, auto & self)
{
return archive (self. name , self. age );
}
std::string name;
int age{};
};
或者使用参数依赖查找:
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
与数据一起或分开创建输入和输出档案:
// 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();
可以从以下任一字节类型创建存档:
// 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);
您还可以使用固定大小的数据对象,例如数组、 std::array
和视图类型,例如与上面类似的std::span
。您只需要确保有足够的大小,因为它们不可调整大小:
// 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);
当使用向量或字符串时,它会自动增长到正确的大小,但是,上面的数据仅限于数组或跨度的边界。
以上述任何方式创建存档时,都可以传递控制存档行为的可变数量的参数,例如字节顺序、默认大小类型、指定附加行为等。自述文件的其余部分对此进行了讨论。
如上所述,该库几乎完全是 constexpr,下面是一个使用数组作为数据对象但也在编译时使用它来序列化和反序列化整数元组的示例:
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 });
为了方便起见,该库还为编译时提供了一些简化的序列化函数:
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()
查询in
和out
的位置,即分别读取和写入的字节:
std:: size_t bytes_read = in.position();
std:: size_t bytes_written = out.position();
向后或向前重置位置,或重置到开始位置,请务必小心使用:
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.
当序列化可变长度标准库类型(例如向量、字符串)和视图类型(例如跨度和字符串视图)时,库首先存储表示大小的 4 字节整数,然后存储元素。
std::vector v = { 1 , 2 , 3 , 4 };
out (v);
in (v);
默认大小类型为 4 字节(即std::uint32_t
)的原因是为了不同架构之间的可移植性,并且大多数程序几乎从未达到容器超过 2^32 项的情况,并且可能是默认情况下付出 8 字节大小的代价是不公平的。
对于不是 4 字节的特定大小类型,请使用zpp::bits::sized
/ zpp::bits::sized_t
,如下所示:
// 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);
确保大小类型对于序列化对象来说足够大,否则根据无符号类型的转换规则,将序列化较少的项目。
您还可以选择根本不序列化大小,如下所示:
// 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);
对于常见的情况,类型的大小/未大小版本有别名声明,例如,这里是vector
和span
,其他诸如string
、 string_view
等都使用相同的模式。
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.
固定大小类型(例如数组、 std::array
、 std::tuple
)的序列化不包含任何开销,除了彼此后面的元素之外。
在创建过程中可以更改整个存档的默认大小类型:
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{});
库知道如何将大多数类型的对象优化和序列化为字节。但是,当使用显式序列化函数时,它会被禁用。
如果您知道您的类型与原始字节一样可序列化,并且您正在使用显式序列化,则可以选择加入并将其序列化优化为纯粹的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));
}
};
也可以直接从向量或普通可复制类型的跨度执行此操作,这次我们使用bytes
而不是as_bytes
因为我们将向量的内容转换为字节而不是向量对象本身(向量指向的数据而不是矢量对象):
std::vector<point> points;
out (zpp::bits::bytes(points));
in (zpp::bits::bytes(points));
然而,在这种情况下,大小未序列化,将来可能会扩展以支持与其他视图类型类似的序列化大小。如果您需要序列化为字节并想要大小,作为解决方法,可以转换为std::span<std::byte>
。
虽然由于序列化的零开销而没有完美的工具来处理结构的向后兼容性,但您可以使用std::variant
作为对类进行版本控制或创建基于多态性的良好调度的方法,具体方法如下:
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
然后是序列化本身:
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);
变体序列化的方式是在序列化实际对象之前将其索引(0 或 1)序列化为std::byte
。这是非常有效的,但是有时用户可能希望为此选择显式序列化 id,请参阅下面的点
要设置自定义序列化 ID,您需要分别在类内部/外部添加一行:
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>;
请注意,变体中类型的序列化 ID 的长度必须匹配,否则将出现编译错误。
您还可以使用任何字节序列而不是可读字符串,以及整数或任何文字类型,下面是如何使用字符串哈希作为序列化 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
您还可以仅序列化哈希的第一个字节,如下所示:
// 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>;
然后在编译时使用 (...等待它) zpp::bits::out
在编译时将该类型转换为字节,因此只要您的文字类型可以根据上述进行序列化,您就可以将其用作序列化 ID。 id 被序列化为std::array<std::byte, N>
但对于 1、2、4 和 8 字节,其基础类型为std::byte
std::uint16_t
、 std::uin32_t
和std::uint64_t
分别是为了易用性和效率。
如果您想序列化没有 id 的变体,或者如果您知道变体在反序列化时将具有特定的 ID,则可以使用zpp::bits::known_id
来包装您的变体:
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));
库中辅助文字的描述:
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.
zpp::bits::apply
将输入存档内容直接应用到函数,该函数必须是非模板并且只有一个重载: 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 ;
}
当你的函数不接收任何参数时,效果只是调用函数而不进行反序列化,返回值是你的函数的返回值。当函数返回 void 时,结果类型没有值。
该库还提供了一个瘦 RPC(远程过程调用)接口,以允许序列化和反序列化函数调用:
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);
关于错误处理,与上面的许多示例类似,您可以使用返回值、异常或zpp::throwing
方式来处理错误。
// 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);
RPC 调用的 ID 可能会被跳过,例如它们被带外传递,以下是实现此目的的方法:
server.serve(id); // id is already known, don't deserialize it.
client.request_body<Id>(arguments...); // request without serializing id.
成员函数也可以为 RPC 注册,但是服务器需要在构造过程中获取对类对象的引用,并且所有成员函数必须属于同一个类(尽管命名空间作用域函数可以混合使用):
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);
RPC 还可以在不透明模式下工作,并让函数本身序列化/反序列化数据,当将函数绑定为不透明时,使用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>
>;
使用的默认字节顺序是本机处理器/操作系统选择的字节顺序。您可以在构造过程中使用zpp::bits::endian
选择另一个字节顺序,如下所示:
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{});
在接收端(输入存档),该库支持 const 字节类型的视图类型,例如std::span<const std::byte>
以便在不复制的情况下获取部分数据的视图。这需要小心使用,因为使包含的数据的迭代器无效可能会导致释放后使用。提供它是为了在需要时进行优化:
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
}
还有一个未调整大小的版本,它消耗其余的存档数据以允许标头然后任意数量的数据的常见用例:
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
该库不支持序列化空指针值,但显式支持可选的拥有指针,例如创建图形和复杂结构。
理论上,使用std::optional<std::unique_ptr<T>>
是有效的,但建议使用专门制作的zpp::bits::optional_ptr<T>
它可以优化可选对象通常保留的布尔值,并使用空指针作为无效状态。
在这种情况下,序列化空指针值将序列化零字节,而非空值则序列化为单个一字节,后跟对象的字节。 (即序列化与std::optional<T>
相同)。
作为库实现的一部分,需要实现一些反射类型,用于计算成员和访问成员,并且库将这些类型公开给用户:
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);
上面的示例可以使用或不使用ZPP_BITS_AUTODETECT_MEMBERS_MODE=1
,具体取决于#if
。如上所述,我们必须依靠特定的编译器功能来检测可能不可移植的成员的数量。
可以使用其他控制选项来构造存档,例如zpp::bits::append{}
它指示输出存档将位置设置为向量或其他数据源的末尾。 (对于输入档案此选项无效)
std::vector<std::byte> data;
zpp::bits::out out (data, zpp::bits::append{});
可以使用多个控件并将它们与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{});
如果输出存档到不断增长的缓冲区,或者使用输入存档来限制单个长度前缀消息的长度,可以使用zpp::bits::alloc_limit<L>{}
来限制分配大小,以避免提前分配非常大的缓冲区zpp::bits::alloc_limit<L>{}
。预期用途是出于安全和理智的原因,而不是准确的分配测量:
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 >{});
为了获得最佳正确性,当使用增长缓冲区进行输出时,如果缓冲区增长了,则最终会根据输出存档的确切位置调整缓冲区大小,这会导致额外的大小调整,这在大多数情况下是可以接受的,但您可以避免这种情况使用position()
额外调整大小并识别缓冲区的末尾。您可以通过使用zpp::bits::no_fit_size{}
来实现此目的:
zpp::bits::out out (data, zpp::bits::no_fit_size{});
要控制输出存档向量的放大,您可以使用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.
默认情况下,为了安全起见,使用不断增长的缓冲区的输出存档会在任何缓冲区增长之前检查溢出。对于 64 位系统,此检查虽然便宜,但几乎是多余的,因为当 64 位整数表示内存大小时,它几乎不可能溢出。 (即,在内存接近溢出该整数之前,内存分配将失败)。如果您希望禁用这些溢出检查以提高性能,请使用: zpp::bits::no_enlarge_overflow{}
:
zpp::bits::out out (data, zpp::bits::no_enlarge_overflow{}); // Disable overflow check when enlarging.
显式序列化时,通常需要识别存档是输入存档还是输出存档,这是通过archive.kind()
静态成员函数完成的,并且可以在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)
}
}
该库提供了一种用于序列化和反序列化可变长度整数的类型:
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;
这是编译时编码的示例:
static_assert (zpp::bits::to_bytes<zpp::bits::varint{ 150 }>() == "9601"_decode_hex);
提供类模板zpp::bits::varint<T, E = varint_encoding::normal>
以便能够定义任何 varint 整型或枚举类型,以及可能的编码zpp::bits::varint_encoding::normal/zig_zag
(默认为正常)。
提供了以下别名声明:
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.
在归档创建过程中,默认情况下也可以使用 varints 来序列化大小:
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 格式。
请注意,protobuf 支持是一种实验性的,这意味着它可能不包括所有可能的 protobuf 功能,并且它通常比默认格式慢(大约慢 2-5 倍,主要是在反序列化方面),默认格式的目标是零开销。
从基本消息开始:
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);
对于完整的语法(我们稍后将使用它来传递更多选项),请使用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{}>;
保留字段:
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
};
要为每个成员显式指定字段编号:
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;
};
访问字段后面的值通常是透明的,但如果明确需要,请使用pb_value(<variable>)
来获取或分配该值。
要将成员映射到另一个字段编号:
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.
};
固定成员只是常规的 C++ 数据成员:
struct example
{
std:: uint32_t i; // fixed unsigned integer 32, field number == 1
};
与zpp::bits::members
一样,当需要时,您可以使用zpp::bits::pb_members<N>
指定协议字段中的成员数量:
struct example
{
using serialize = zpp::bits::pb_members< 1 >; // 1 member.
zpp::bits:: vint32_t i; // field number == 1
};
上述的完整版本涉及将成员数量作为第二个参数传递给协议:
struct example
{
using serialize = zpp::bits::protocol<zpp::bits::pb{}, 1 >; // 1 member.
zpp::bits:: vint32_t i; // field number == 1
};
嵌入消息只是作为数据成员嵌套在类中:
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);
重复字段采用所属容器的形式:
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
};
目前,所有字段都是可选的,这是一个很好的做法,为了提高效率,丢失的字段将被删除并且不会连接到消息。消息中未设置的任何值都会使目标数据成员保持不变,这允许通过使用非静态数据成员初始值设定项实现数据成员的默认值,或者在反序列化消息之前初始化数据成员。
让我们获取完整的.proto
文件并翻译它:
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 ;
}
翻译后的文件:
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;
反序列化最初用 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
我们得到的person
的输出是:
name : "John Doe"
id : 1234
email : "[email protected]"
phones {
number : "555-4321"
type : home
}
让我们将其序列化:
person . SerializeToString ()
结果是:
b' n x08 John Doe x10 xd2 t x1a x10 [email protected]" x0c n x08 555-4321 x10 x01 '
回到C++:
using namespace zpp ::bits::literals ;
constexpr auto data =
" nx08 John Doe x10xd2tx1ax10 [email protected] "x0cnx08 "
" 555-4321 x10x01 " _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
默认情况下, zpp::bits
会积极内联,但为了减少代码大小,它不会内联 varint(可变长度整数)的完整解码。要配置完整 varint 解码的内联,请定义ZPP_BITS_INLINE_DECODE_VARINT=1
。
如果您怀疑zpp::bits
内联太多以至于严重影响代码大小,您可以定义ZPP_BITS_INLINE_MODE=0
,这将禁用所有强制内联并观察结果。通常它的影响可以忽略不计,但它是按原样提供的,用于额外的控制。
在某些编译器中,您可能会发现递归结构(例如树形图)总是内联失败。在这些情况下,需要以某种方式避免特定结构的始终内联属性,一个简单的例子是使用显式序列化函数,尽管大多数时候库会检测到这种情况并且没有必要,但仅提供了示例以防万一:
struct node
{
constexpr static auto serialize ( auto & archive, auto & node)
{
return archive (node. value , node. nodes );
}
int value;
std::vector<node> nodes;
};
图书馆 | 测试用例 | 垃圾箱尺寸 | 数据大小 | 服务时间 | 德斯时间 |
---|---|---|---|---|---|
zpp_位 | 一般的 | 52192B | 8413B | 733毫秒 | 693毫秒 |
zpp_位 | 固定缓冲区 | 48000B | 8413B | 620毫秒 | 667毫秒 |
比瑟里 | 一般的 | 70904B | 6913B | 1470毫秒 | 1524毫秒 |
比瑟里 | 固定缓冲区 | 53648B | 6913B | 927毫秒 | 1466毫秒 |
促进 | 一般的 | 279024B | 11037B | 15126毫秒 | 12724毫秒 |
谷物 | 一般的 | 70560B | 10413B | 10777毫秒 | 9088毫秒 |
平面缓冲区 | 一般的 | 70640B | 14924B | 8757毫秒 | 3361毫秒 |
手写的 | 一般的 | 47936B | 10413B | 1506毫秒 | 1577毫秒 |
手写的 | 不安全 | 47944B | 10413B | 1616毫秒 | 1392毫秒 |
输出流 | 一般的 | 53872B | 8413B | 11956毫秒 | 12928毫秒 |
消息包 | 一般的 | 89144B | 8857B | 2770毫秒 | 14033毫秒 |
原始缓冲区 | 一般的 | 2077864B | 10018B | 19929毫秒 | 20592毫秒 |
原始缓冲区 | 竞技场 | 2077872B | 10018B | 10319毫秒 | 11787毫秒 |
亚斯 | 一般的 | 61072B | 10463B | 2286毫秒 | 1770毫秒 |
图书馆 | 测试用例 | 垃圾箱尺寸 | 数据大小 | 服务时间 | 德斯时间 |
---|---|---|---|---|---|
zpp_位 | 一般的 | 47128B | 8413B | 790毫秒 | 715毫秒 |
zpp_位 | 固定缓冲区 | 43056B | 8413B | 605毫秒 | 694毫秒 |
比瑟里 | 一般的 | 53728B | 6913B | 2128毫秒 | 1832毫秒 |
比瑟里 | 固定缓冲区 | 49248B | 6913B | 946毫秒 | 1941毫秒 |
促进 | 一般的 | 237008B | 11037B | 16011毫秒 | 13017毫秒 |
谷物 | 一般的 | 61480B | 10413B | 9977毫秒 | 8565毫秒 |
平面缓冲区 | 一般的 | 62512B | 14924B | 9812毫秒 | 3472毫秒 |
手写的 | 一般的 | 43112B | 10413B | 1391毫秒 | 1321毫秒 |
手写的 | 不安全 | 43120B | 10413B | 1393毫秒 | 1212毫秒 |
输出流 | 一般的 | 48632B | 8413B | 10992毫秒 | 12771毫秒 |
消息包 | 一般的 | 77384B | 8857B | 3563毫秒 | 14705毫秒 |
原始缓冲区 | 一般的 | 2032712B | 10018B | 18125毫秒 | 20211毫秒 |
原始缓冲区 | 竞技场 | 2032760B | 10018B | 9166毫秒 | 11378毫秒 |
亚斯 | 一般的 | 51000B | 10463B | 2114毫秒 | 1558毫秒 |
我希望您发现这个库很有用。请随时提交任何问题、提出改进建议等。