단 하나의 헤더 파일을 포함하는 최신 C++20 바이너리 직렬화 및 RPC 라이브러리입니다.
이 라이브러리는 zpp::serializer의 후속 라이브러리입니다. 라이브러리는 사용하기 더 간단해지려고 노력하지만 이전 라이브러리와 다소 유사한 API를 가지고 있습니다.
constexpr
입니다.zpp::bits
zpp::serializer
보다 유형 지정이 더 쉽습니다.zpp::serializer
전역 다형성 유형과 비교됩니다. 많은 유형의 경우 직렬화 활성화는 투명하며 추가 코드 줄이 필요하지 않습니다. 이러한 유형은 배열 멤버가 아닌 집계 유형이어야 합니다. 다음은 이름과 나이가 포함된 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 ;
});
}
또 다른 옵션은 오류 검사가 두 개의 간단한 co_await
로 바뀌는 zpp::throwing입니다. 오류 검사 방법을 이해하기 위해 전체 주요 기능을 제공합니다.
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);
벡터나 문자열을 사용하면 자동으로 올바른 크기로 커지지만, 위의 경우 데이터는 배열이나 범위의 경계로 제한됩니다.
위의 방법 중 하나로 아카이브를 생성할 때 바이트 순서, 기본 크기 유형, 추가 동작 지정 등과 같은 아카이브 동작을 제어하는 가변 개수의 매개변수를 전달할 수 있습니다. 이에 대해서는 나머지 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
s, std::tuple
s와 같은 고정 크기 유형의 직렬화에는 요소 뒤에 서로 이어지는 요소를 제외하고 오버헤드가 포함되지 않습니다.
생성 중에 전체 아카이브의 기본 크기 유형을 변경할 수 있습니다.
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
사용하여 입력 아카이브 내용을 함수에 직접 적용할 수 있습니다. 함수는 템플릿이 아니어야 하며 정확히 하나의 오버로드가 있어야 합니다. 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를 건너뛸 수 있습니다. 예를 들어 RPC 호출이 대역 외로 전달되는 경우 이를 달성하는 방법은 다음과 같습니다.
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
라이브러리는 널 포인터 값 직렬화를 지원하지 않지만 그래프 및 복잡한 구조 생성과 같은 선택적 소유 포인터를 명시적으로 지원합니다.
이론적으로는 std::optional<std::unique_ptr<T>>
사용하는 것이 유효하지만 선택적 개체가 일반적으로 유지하는 부울을 최적화하는 특별히 만들어진 zpp::bits::optional_ptr<T>
사용하는 것이 좋습니다. 널 포인터를 유효하지 않은 상태로 사용합니다.
이 경우 널 포인터 값을 직렬화하면 0바이트가 직렬화되고, 널이 아닌 값은 단일 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.
아카이브 생성 중에 varint를 사용하여 기본적으로 크기를 직렬화하는 것도 가능합니다.
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 기능을 포함하지 않을 수 있으며 일반적으로 오버헤드 0을 목표로 하는 기본 형식보다 느립니다(주로 역직렬화에서 약 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_bits | 일반적인 | 52192B | 8413B | 733ms | 693ms |
zpp_bits | 고정 버퍼 | 48000B | 8413B | 620ms | 667ms |
쓰라린 | 일반적인 | 70904B | 6913B | 1470ms | 1524ms |
쓰라린 | 고정 버퍼 | 53648B | 6913B | 927ms | 1466ms |
후원 | 일반적인 | 279024B | 11037B | 15126ms | 12724ms |
시리얼 | 일반적인 | 70560B | 10413B | 10777ms | 9088ms |
플랫버퍼 | 일반적인 | 70640B | 14924B | 8757ms | 3361ms |
손으로 쓴 | 일반적인 | 47936B | 10413B | 1506ms | 1577ms |
손으로 쓴 | 위험한 | 47944B | 10413B | 1616ms | 1392ms |
아이오스트림 | 일반적인 | 53872B | 8413B | 11956ms | 12928ms |
msgpack | 일반적인 | 89144B | 8857B | 2770ms | 14033ms |
프로토부프 | 일반적인 | 2077864B | 10018B | 19929ms | 20592ms |
프로토부프 | 투기장 | 2077872B | 10018B | 10319ms | 11787ms |
야 | 일반적인 | 61072B | 10463B | 2286ms | 1770ms |
도서관 | 테스트 케이스 | 빈 크기 | 데이터 크기 | 지금 시간 | 시간 |
---|---|---|---|---|---|
zpp_bits | 일반적인 | 47128B | 8413B | 790ms | 715ms |
zpp_bits | 고정 버퍼 | 43056B | 8413B | 605ms | 694ms |
쓰라린 | 일반적인 | 53728B | 6913B | 2128ms | 1832ms |
쓰라린 | 고정 버퍼 | 49248B | 6913B | 946ms | 1941ms |
후원 | 일반적인 | 237008B | 11037B | 16011ms | 13017ms |
시리얼 | 일반적인 | 61480B | 10413B | 9977ms | 8565ms |
플랫버퍼 | 일반적인 | 62512B | 14924B | 9812ms | 3472ms |
손으로 쓴 | 일반적인 | 43112B | 10413B | 1391ms | 1321ms |
손으로 쓴 | 위험한 | 43120B | 10413B | 1393ms | 1212ms |
아이오스트림 | 일반적인 | 48632B | 8413B | 10992ms | 12771ms |
msgpack | 일반적인 | 77384B | 8857B | 3563ms | 14705ms |
프로토부프 | 일반적인 | 2032712B | 10018B | 18125ms | 20211ms |
프로토부프 | 투기장 | 2032760B | 10018B | 9166ms | 11378ms |
야 | 일반적인 | 51000B | 10463B | 2114ms | 1558ms |
이 라이브러리가 도움이 되기를 바랍니다. 문제점, 개선사항 제안 등을 자유롭게 보내주시기 바랍니다.