一種現代 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::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毫秒 |
我希望您發現這個庫很有用。請隨時提交任何問題、提出改進建議等。