最新の C++20 バイナリ シリアル化および RPC ライブラリ。ヘッダー ファイルは 1 つだけです。
このライブラリは、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 ;
});
}
もう 1 つのオプションは、エラー チェックが 2 つの単純なco_await
に変わる zpp::throwing です。エラーをチェックする方法を理解するために、完全な 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
- null ポインターまたは値のないバリアントをシリアル化しようとしています。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);
ベクトルまたは文字列を使用すると、適切なサイズに自動的に拡大されますが、上記の場合、データは配列またはスパンの境界に制限されます。
上記のいずれかの方法でアーカイブを作成する場合、バイト順序、デフォルトのサイズ タイプ、追加動作の指定など、アーカイブの動作を制御する可変個数のパラメータを渡すことができます。これについては、README の残りの部分で説明します。
上で述べたように、ライブラリはほぼ完全に 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));
}
};
これを簡単にコピー可能な型のベクトルまたはスパンから直接行うこともできます。今回はas_bytes
の代わりに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
を使用して、入力アーカイブの内容を関数に直接適用できます。その関数はテンプレートではなく、オーバーロードを 1 つだけ持つ必要があります。 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>
>;
使用されるデフォルトのバイト順序は、ネイティブ プロセッサ/OS が選択したものです。次のように構築中に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{});
受信側 (入力アーカイブ) では、コピーせずにデータの一部のビューを取得するために、ライブラリはstd::span<const std::byte>
などの const バイト型のビュー タイプをサポートします。含まれているデータのイテレータを無効にすると、解放後に使用される可能性があるため、これは慎重に使用する必要があります。必要に応じて最適化を可能にするために提供されています。
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
このライブラリは、null ポインター値のシリアル化をサポートしていませんが、グラフや複雑な構造を作成するためなど、オプションの所有ポインターを明示的にサポートしています。
理論的には、 std::optional<std::unique_ptr<T>>
を使用することが有効ですが、オプションのオブジェクトが通常保持するブール値を最適化する、特別に作成されたzpp::bits::optional_ptr<T>
を使用することをお勧めします。無効な状態として null ポインタを使用します。
この場合、null ポインター値をシリアル化すると 0 バイトがシリアル化されますが、null 以外の値は単一の 1 バイトとその後にオブジェクトのバイトが続くようにシリアル化されます。 (つまり、シリアル化は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);
上記の例は、 #if
に応じて、 ZPP_BITS_AUTODETECT_MEMBERS_MODE=1
有無にかかわらず動作します。上で述べたように、移植できない可能性のあるメンバーの数を検出するには、特定のコンパイラ機能に依存する必要があります。
アーカイブは、ベクトルまたは他のデータ ソースの末尾に位置を設定するように出力アーカイブに指示する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>
可能なエンコーディングzpp::bits::varint_encoding::normal/zig_zag
とともに、任意の 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.
デフォルトでバリアントを使用してサイズをシリアル化することは、アーカイブの作成時にも可能です。
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
};
上記の完全版では、メンバーの数を 2 番目のパラメーターとしてプロトコルに渡します。
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
積極的にインライン化しますが、コード サイズを削減するために、バリアント (可変長整数) の完全なデコードをインライン化しません。完全な Variant デコードのインライン化を構成するには、 ZPP_BITS_INLINE_DECODE_VARINT=1
を定義します。
zpp::bits
コード サイズに悪影響を与えるほどインライン化されすぎていると思われる場合は、 ZPP_BITS_INLINE_MODE=0
を定義して、すべての強制インライン化を無効にして結果を観察します。通常、その影響は無視できますが、追加の制御のためにそのまま提供されます。
一部のコンパイラでは、再帰構造 (ツリー グラフなど) で常にインラインが失敗することがあります。このような場合、特定の構造体に対する always inline 属性を何らかの方法で回避する必要があります。簡単な例としては、明示的なシリアル化関数を使用することが挙げられます。ただし、ほとんどの場合、ライブラリはそのような状況を検出するため、必要ありませんが、この例は単に提供されています。万一に備えて:
struct node
{
constexpr static auto serialize ( auto & archive, auto & node)
{
return archive (node. value , node. nodes );
}
int value;
std::vector<node> nodes;
};
図書館 | テストケース | ビンのサイズ | データサイズ | サーバー時間 | 時間です |
---|---|---|---|---|---|
zpp_bits | 一般的な | 52192B | 8413B | 733ミリ秒 | 693ミリ秒 |
zpp_bits | 固定バッファ | 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ミリ秒 |
iostream | 一般的な | 53872B | 8413B | 11956ミリ秒 | 12928ミリ秒 |
メッセージパック | 一般的な | 89144B | 8857B | 2770ミリ秒 | 14033ミリ秒 |
プロトブフ | 一般的な | 2077864B | 10018B | 19929ミリ秒 | 20592ミリ秒 |
プロトブフ | アリーナ | 2077872B | 10018B | 10319ミリ秒 | 11787ミリ秒 |
やっす | 一般的な | 61072B | 10463B | 2286ミリ秒 | 1770ミリ秒 |
図書館 | テストケース | ビンのサイズ | データサイズ | サーバー時間 | 時間です |
---|---|---|---|---|---|
zpp_bits | 一般的な | 47128B | 8413B | 790ミリ秒 | 715ミリ秒 |
zpp_bits | 固定バッファ | 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ミリ秒 |
iostream | 一般的な | 48632B | 8413B | 10992ミリ秒 | 12771ミリ秒 |
メッセージパック | 一般的な | 77384B | 8857B | 3563ミリ秒 | 14705ミリ秒 |
プロトブフ | 一般的な | 2032712B | 10018B | 18125ミリ秒 | 20211ms |
プロトブフ | アリーナ | 2032760B | 10018B | 9166ミリ秒 | 11378ミリ秒 |
やっす | 一般的な | 51000B | 10463B | 2114ミリ秒 | 1558ミリ秒 |
このライブラリがお役に立てば幸いです。問題点や改善点など、お気軽にご提出ください。