การทำให้เป็นอนุกรมไบนารี C++20 ที่ทันสมัยและไลบรารี RPC โดยมีไฟล์ส่วนหัวเพียงไฟล์เดียว
ไลบรารีนี้เป็นตัวตายตัวแทนของ zpp::serializer ไลบรารีพยายามที่จะใช้งานได้ง่ายขึ้น แต่มี API ที่คล้ายคลึงกับรุ่นก่อนไม่มากก็น้อย
constexpr
zpp::bits
จะพิมพ์ได้ง่ายกว่า zpp::serializer
zpp::serializer
มีรหัสการทำให้เป็นอนุกรม sha1 คงที่ 8 ไบต์ สำหรับหลายประเภท การเปิดใช้งานการทำให้เป็นอนุกรมมีความโปร่งใสและไม่ต้องใช้โค้ดเพิ่มเติม ประเภทเหล่านี้จำเป็นต้องเป็นประเภทรวม โดยไม่มีสมาชิกอาร์เรย์ นี่คือตัวอย่างคลาส 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()
(อ่านว่า "succeed 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::throw โดยที่การตรวจสอบข้อผิดพลาดกลายเป็น co_await
ง่ายๆ สองรายการ เพื่อให้เข้าใจวิธีการตรวจสอบข้อผิดพลาด เรามีฟังก์ชันหลักครบถ้วน:
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 ทำงานร่วมกับ requires expression
ภายใต้ if constexpr
และ unevaluated lambda 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, ...
เป็นสมาชิกข้อมูลที่ไม่คงที่ในชั้นเรียนของคุณ
นี่คือตัวอย่างของคลาสบุคคลอีกครั้งที่มีฟังก์ชันการทำให้เป็นอนุกรมที่ชัดเจน:
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 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 });
ค้นหาตำแหน่งของ in
และ out
โดยใช้ position()
หรืออีกนัยหนึ่งคือไบต์ที่อ่านและเขียนตามลำดับ:
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));
}
};
นอกจากนี้ยังเป็นไปได้ที่จะทำเช่นนี้ได้โดยตรงจากเวกเตอร์หรือสแปนของประเภทที่คัดลอกได้เล็กน้อย คราวนี้เราใช้ 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
ก่อนที่จะซีเรียลไลซ์วัตถุจริง สิ่งนี้มีประสิทธิภาพมาก แต่บางครั้งผู้ใช้อาจต้องการเลือกรหัสซีเรียลไลซ์ที่ชัดเจน โปรดดูที่จุดด้านล่าง
หากต้องการตั้งค่ารหัสซีเรียลไลซ์แบบกำหนดเอง คุณต้องเพิ่มบรรทัดเพิ่มเติมทั้งภายในและภายนอกชั้นเรียนของคุณตามลำดับ:
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>;
โปรดทราบว่ารหัสการทำให้ซีเรียลไลซ์ประเภทในรูปแบบต่างๆ จะต้องมีความยาวตรงกัน มิฉะนั้นจะเกิดปัญหาข้อผิดพลาดในการคอมไพล์
นอกจากนี้คุณยังอาจใช้ลำดับไบต์แทนสตริงที่อ่านได้ เช่นเดียวกับจำนวนเต็มหรือประเภทตัวอักษรใดๆ ต่อไปนี้คือตัวอย่างวิธีใช้แฮชของสตริงเป็นรหัสซีเรียลไลซ์:
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
ณ เวลาคอมไพล์ ดังนั้นตราบใดที่ประเภทตัวอักษรของคุณสามารถทำให้เป็นอนุกรมได้ตามที่กล่าวข้างต้น คุณสามารถใช้มันเป็น รหัสการทำให้เป็นอนุกรม รหัสถูกทำให้เป็นอนุกรมเป็น std::array<std::byte, N>
อย่างไรก็ตามสำหรับ 1, 2, 4 และ 8 ไบต์ประเภทพื้นฐานของมันคือ std::byte
std::uint16_t
, std::uin32_t
และ std::uint64_t
ตามลำดับเพื่อความสะดวกในการใช้งานและมีประสิทธิภาพ
หากคุณต้องการซีเรียลไลซ์ตัวแปรโดยไม่มีรหัส หรือหากคุณรู้ว่าตัวแปรนั้นจะมี 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 ;
}
เมื่อฟังก์ชันของคุณไม่ได้รับพารามิเตอร์ ผลที่ได้คือการเรียกใช้ฟังก์ชันโดยไม่ต้องดีซีเรียลไลซ์ และค่าที่ส่งคืนคือค่าที่ส่งคืนของฟังก์ชันของคุณ เมื่อฟังก์ชันคืนค่าเป็นโมฆะ จะไม่มีค่าสำหรับประเภทผลลัพธ์
ไลบรารียังมีอินเทอร์เฟซ 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);
เป็นไปได้ที่ ID ของการเรียก 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>
ที่สร้างขึ้นมาโดยเฉพาะ ซึ่งจะปรับบูลีนที่ออบเจ็กต์ทางเลือกมักจะเก็บไว้ให้เหมาะสม และใช้ตัวชี้ null เป็นสถานะที่ไม่ถูกต้อง
การทำให้ค่าตัวชี้ว่างเป็นอนุกรมในกรณีนั้นจะทำให้เป็นอนุกรมไบต์เป็นศูนย์ ในขณะที่ค่าที่ไม่ใช่ค่าว่างจะทำให้เป็นอนุกรมเป็นไบต์เดียวตามด้วยไบต์ของวัตถุ (เช่น การทำให้เป็นอนุกรมจะเหมือนกับ 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
จะอินไลน์เชิงรุก แต่เพื่อลดขนาดโค้ด จะไม่อินไลน์การถอดรหัส varints แบบเต็ม (จำนวนเต็มความยาวตัวแปร) หากต้องการกำหนดค่าอินไลน์ของการถอดรหัส 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 | 1524มิลลิวินาที |
ขี้เมา | บัฟเฟอร์คงที่ | 53648B | 6913B | 927ms | 1466มิลลิวินาที |
เพิ่ม | ทั่วไป | 279024B | 11037B | 15126มิลลิวินาที | 12724มิลลิวินาที |
ซีเรียล | ทั่วไป | 70560B | 10413B | 10777มิลลิวินาที | 9088ms |
บัฟเฟอร์แบบแบน | ทั่วไป | 70640B | 14924B | 8757มิลลิวินาที | 3361มิลลิวินาที |
เขียนด้วยลายมือ | ทั่วไป | 47936B | 10413B | 1506มิลลิวินาที | 1,577มิลลิวินาที |
เขียนด้วยลายมือ | ไม่ปลอดภัย | 47944B | 10413B | 1616มิลลิวินาที | 1392มิลลิวินาที |
ไอโอสตรีม | ทั่วไป | 53872B | 8413B | 11956มิลลิวินาที | 12928ms |
แพ็คข้อความ | ทั่วไป | 89144B | 8857B | 2770มิลลิวินาที | 14033มิลลิวินาที |
โปรโตบุฟ | ทั่วไป | 2077864B | 10018B | 19929มิลลิวินาที | 20592มิลลิวินาที |
โปรโตบุฟ | สนามกีฬา | 2077872B | 10018B | 10319มิลลิวินาที | 11787มิลลิวินาที |
ใช่แล้ว | ทั่วไป | 61072B | 10463B | 2286ms | 1770ms |
ห้องสมุด | กรณีทดสอบ | ขนาดถัง | ขนาดข้อมูล | เวลาเซิร์ฟเวอร์ | ถึงเวลาแล้ว |
---|---|---|---|---|---|
zpp_bits | ทั่วไป | 47128B | 8413B | 790ms | 715ms |
zpp_bits | บัฟเฟอร์คงที่ | 43056B | 8413B | 605ms | 694ms |
ขี้เมา | ทั่วไป | 53728B | 6913B | 2128ms | 1832มิลลิวินาที |
ขี้เมา | บัฟเฟอร์คงที่ | 49248B | 6913B | 946ms | 1941มิลลิวินาที |
เพิ่ม | ทั่วไป | 237008B | 11037B | 16011มิลลิวินาที | 13017มิลลิวินาที |
ซีเรียล | ทั่วไป | 61480B | 10413B | 9977มิลลิวินาที | 8565มิลลิวินาที |
บัฟเฟอร์แบบแบน | ทั่วไป | 62512B | 14924B | 9812มิลลิวินาที | 3472มิลลิวินาที |
เขียนด้วยลายมือ | ทั่วไป | 43112B | 10413B | 1391มิลลิวินาที | 1321มิลลิวินาที |
เขียนด้วยลายมือ | ไม่ปลอดภัย | 43120B | 10413B | 1393มิลลิวินาที | 1212มิลลิวินาที |
ไอโอสตรีม | ทั่วไป | 48632B | 8413B | 1,0992มิลลิวินาที | 12771มิลลิวินาที |
แพ็คข้อความ | ทั่วไป | 77384B | 8857B | 3563มิลลิวินาที | 14705มิลลิวินาที |
โปรโตบุฟ | ทั่วไป | 2032712B | 10018B | 18125มิลลิวินาที | 20211มิลลิวินาที |
โปรโตบุฟ | สนามกีฬา | 2032760B | 10018B | 9166มิลลิวินาที | 11378มิลลิวินาที |
ใช่แล้ว | ทั่วไป | 51000B | 10463B | 2114มิลลิวินาที | 1558มิลลิวินาที |
ฉันหวังว่าคุณจะพบว่าห้องสมุดนี้มีประโยชน์ โปรดอย่าลังเลที่จะส่งปัญหาใด ๆ ให้คำแนะนำในการปรับปรุง ฯลฯ