Làm thế nào để bạn tuần tự hóa một đối tượng trong C ++?


83

Tôi có một hệ thống phân cấp nhỏ của các đối tượng mà tôi cần tuần tự hóa và truyền qua kết nối ổ cắm. Tôi cần cả tuần tự hóa đối tượng, sau đó giải mã hóa nó dựa trên loại nó là gì. Có cách nào dễ dàng để làm điều này trong C ++ (như trong Java) không?

Có bất kỳ hướng dẫn hoặc mẫu mã trực tuyến tuần tự hóa C ++ nào không?

CHỈNH SỬA: Nói rõ ràng, tôi đang tìm kiếm các phương pháp chuyển đổi một đối tượng thành một mảng byte, sau đó trở lại thành một đối tượng. Tôi có thể xử lý việc truyền ổ cắm.


3
Kiểm tra google :: protobuf , nó là thư viện rất mạnh và nhanh để tuần tự hóa nhị phân. Chúng tôi đã sử dụng nó thành công với boost :: asio, v.v.
Ketan

Hãy xem [STLPLUS] [1], với sự kiên trì triển khai. [1]: stlplus.sourceforge.net
lsalamon

4
Các câu trả lời được cung cấp không thực sự giải thích làm thế nào để tuần tự hóa. Một người cung cấp thư viện tuần tự hóa tăng cường, người kia giải thích gotchas trong một triển khai ngây thơ. Vì đây là câu hỏi thường gặp trong c ++ , ai đó thực sự có thể trả lời nó không?
ẩn danh

Câu trả lời:


55

Nói về tuần tự hóa, API tuần tự hóa tăng cường xuất hiện trong tâm trí tôi. Đối với việc truyền dữ liệu tuần tự qua mạng, tôi sẽ sử dụng ổ cắm Berkeley hoặc thư viện asio .

Chỉnh sửa:
Nếu bạn muốn tuần tự hóa các đối tượng của mình thành một mảng byte, bạn có thể sử dụng trình tuần tự tăng cường theo cách sau (lấy từ trang web hướng dẫn):

#include <boost/archive/binary_oarchive.hpp>
#include <boost/archive/binary_iarchive.hpp>
class gps_position
{
private:
    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & degrees;
        ar & minutes;
        ar & seconds;
    }
    int degrees;
    int minutes;
    float seconds;

public:
    gps_position(){};
    gps_position(int d, int m, float s) :
    degrees(d), minutes(m), seconds(s)
    {}
};

Việc tuần tự hóa thực tế sau đó khá dễ dàng:

#include <fstream>
std::ofstream ofs("filename.dat", std::ios::binary);

    // create class instance
    const gps_position g(35, 59, 24.567f);

    // save data to archive
    {
        boost::archive::binary_oarchive oa(ofs);
        // write class instance to archive
        oa << g;
        // archive and stream closed when destructors are called
    }

Deserialization hoạt động theo cách tương tự.

Ngoài ra còn có các cơ chế cho phép bạn xử lý tuần tự hóa con trỏ (cấu trúc dữ liệu phức tạp như tress, v.v. không có vấn đề gì), các lớp dẫn xuất và bạn có thể chọn giữa tuần tự hóa văn bản và nhị phân. Bên cạnh đó, tất cả các thùng chứa STL đều được hỗ trợ ra khỏi hộp.


Đây là một câu hỏi c ++, tại sao lớp gps_position lại nạp chồng toán tử <<. Không có bất kỳ chức năng kết bạn nào được xác định
Vicente Bolea

chú ý đến "friend class boost :: serialization :: access". Điều này cung cấp quyền truy cập các chức năng của thư viện tuần tự hóa cho các thành viên lớp ngay cả khi họ là riêng tư.
Robert Ramey

13

Trong một số trường hợp, khi xử lý các loại đơn giản, bạn có thể làm:

object o;
socket.write(&o, sizeof(o));

Điều đó được chấp nhận dưới dạng bằng chứng khái niệm hoặc bản thảo đầu tiên, vì vậy các thành viên khác trong nhóm của bạn có thể tiếp tục làm việc trên các phần khác.

Nhưng sớm hay muộn, thường là sớm hơn , điều này sẽ khiến bạn bị tổn thương!

Bạn gặp sự cố với:

  • Bảng con trỏ ảo sẽ bị hỏng.
  • Con trỏ (đến dữ liệu / thành viên / chức năng) sẽ bị hỏng.
  • Sự khác biệt về đệm / căn chỉnh trên các máy khác nhau.
  • Vấn đề sắp xếp byte Big / Little-Endian.
  • Các biến thể trong việc thực hiện float / double.

(Ngoài ra, bạn cần biết những gì bạn đang giải nén ở phía nhận.)

Bạn có thể cải thiện điều này bằng cách phát triển các phương pháp sắp xếp / bỏ quản lý của riêng bạn cho mọi lớp. (Lý tưởng nhất là ảo, vì vậy chúng có thể được mở rộng trong các lớp con.) Một vài macro đơn giản sẽ cho phép bạn viết ra các kiểu cơ bản khác nhau khá nhanh chóng theo thứ tự lớn / nhỏ-endian-trung lập.

Nhưng loại công việc khó chịu đó tốt hơn nhiều và dễ dàng hơn, được xử lý thông qua thư viện tuần tự hóa của boost .


Đó là điều mà tôi đã suy nghĩ. Nhưng khi tôi muốn nối tiếp vào một luồng mạng, điều này hoàn toàn không hoạt động. Nhiều nhất là vì tính bền và các nền tảng khác nhau. Nhưng tôi không biết rằng nó làm hỏng con trỏ ảo. Cảm ơn =)
Atmocreations

4

Có một mẫu chung mà bạn có thể sử dụng để tuần tự hóa các đối tượng. Nguyên thủy Fundemental là hai hàm này bạn có thể đọc và ghi từ các trình vòng lặp:

template <class OutputCharIterator>
void putByte(char byte, OutputCharIterator &&it)
{
    *it = byte;
    ++it;
}


template <class InputCharIterator>
char getByte(InputCharIterator &&it, InputCharIterator &&end)
{
    if (it == end)
    {
        throw std::runtime_error{"Unexpected end of stream."};
    }

    char byte = *it;
    ++it;
    return byte;
}

Sau đó, các chức năng tuần tự hóa và giải mã hóa tuân theo mẫu:

template <class OutputCharIterator>
void serialize(const YourType &obj, OutputCharIterator &&it)
{
    // Call putbyte or other serialize overloads.
}

template <class InputCharIterator>
void deserialize(YourType &obj, InputCharIterator &&it, InputCharIterator &&end)
{
    // Call getByte or other deserialize overloads.
}

Đối với các lớp, bạn có thể sử dụng mẫu hàm bạn bè để cho phép tìm thấy quá tải bằng cách sử dụng ADL:

class Foo
{
    int internal1, internal2;

    // So it can be found using ADL and it accesses private parts.
    template <class OutputCharIterator>
    friend void serialize(const Foo &obj, OutputCharIterator &&it)
    {
        // Call putByte or other serialize overloads.
    }

    // Deserialize similar.
};

Trong chương trình của bạn, bạn có thể tuần tự hóa và đối tượng thành một tệp như sau:

std::ofstream file("savestate.bin");
serialize(yourObject, std::ostreambuf_iterator<char>(file));

Sau đó đọc:

std::ifstream file("savestate.bin");
deserialize(yourObject, std::istreamBuf_iterator<char>(file), std::istreamBuf_iterator<char>());

Câu trả lời cũ của tôi ở đây:

Serialization có nghĩa là biến đối tượng của bạn thành dữ liệu nhị phân. Trong khi deserialization có nghĩa là tạo lại một đối tượng từ dữ liệu.

Khi tuần tự hóa, bạn đang đẩy các byte vào một uint8_tvector. Khi hủy số hóa, bạn đang đọc các byte từ mộtuint8_t vectơ.

Chắc chắn có những mẫu bạn có thể sử dụng khi sắp xếp thứ tự.

Mỗi lớp có thể nối tiếp hóa phải có serialize(std::vector<uint8_t> &binaryData) hoặc hàm có ký tự tương tự sẽ ghi biểu diễn nhị phân của nó vào vectơ được cung cấp. Sau đó, hàm này có thể chuyển vectơ này xuống các hàm tuần tự hóa của thành viên để họ cũng có thể viết nội dung của mình vào đó.

Vì cách biểu diễn dữ liệu có thể khác nhau trên các kiến ​​trúc khác nhau. Bạn cần tìm ra một lược đồ cách biểu diễn dữ liệu.

Hãy bắt đầu từ những điều cơ bản:

Sắp xếp thứ tự dữ liệu số nguyên

Chỉ cần viết các byte theo thứ tự endian nhỏ. Hoặc sử dụng biểu diễn biến thể nếu kích thước quan trọng.

Serialization theo thứ tự endian nhỏ:

data.push_back(integer32 & 0xFF);
data.push_back((integer32 >> 8) & 0xFF);
data.push_back((integer32 >> 16) & 0xFF);
data.push_back((integer32 >> 24) & 0xFF);

Hủy đăng ký từ trật tự endian nhỏ:

integer32 = data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24);

Tuần tự hóa dữ liệu dấu phẩy động

Theo như tôi biết IEEE 754 có độc quyền ở đây. Tôi không biết bất kỳ kiến ​​trúc chính thống nào sẽ sử dụng thứ gì khác cho phao nổi. Điều duy nhất có thể khác là thứ tự byte. Một số kiến ​​trúc sử dụng ít endian, những kiến ​​trúc khác sử dụng thứ tự byte endian lớn. Điều này có nghĩa là bạn cần phải cẩn thận thứ tự nào để bạn tăng byte ở đầu nhận. Một sự khác biệt khác có thể là xử lý các giá trị không chuẩn và vô cực và NAN. Nhưng miễn là bạn tránh những giá trị này, bạn sẽ ổn.

Serialization:

uint8_t mem[8];
memcpy(mem, doubleValue, 8);
data.push_back(mem[0]);
data.push_back(mem[1]);
...

Deserialization đang làm nó lạc hậu. Lưu ý thứ tự byte của kiến ​​trúc của bạn!

Serializing chuỗi

Trước tiên, bạn cần đồng ý về một bảng mã. UTF-8 là phổ biến. Sau đó, lưu trữ nó dưới dạng tiền tố độ dài: đầu tiên bạn lưu trữ độ dài của chuỗi bằng phương pháp tôi đã đề cập ở trên, sau đó viết chuỗi theo từng byte.

Nối tiếp các mảng.

Chúng giống như một chuỗi. Đầu tiên bạn tuần tự hóa một số nguyên đại diện cho kích thước của mảng sau đó tuần tự hóa từng đối tượng trong đó.

Nối tiếp toàn bộ các đối tượng

Như tôi đã nói trước đây, họ nên có một serializephương thức thêm nội dung vào một vector. Để giải mã một đối tượng, nó phải có một hàm tạo nhận luồng byte. Nó có thể là một istreamnhưng trong trường hợp đơn giản nhất, nó có thể chỉ là một tham chiếuuint8_t con trỏ . Hàm tạo đọc các byte mà nó muốn từ luồng và thiết lập các trường trong đối tượng. Nếu hệ thống được thiết kế tốt và tuần tự hóa các trường theo thứ tự trường đối tượng, bạn chỉ có thể chuyển luồng tới các hàm tạo của trường trong danh sách trình khởi tạo và sắp xếp chúng được giải mã theo đúng thứ tự.

Sắp xếp thứ tự các đồ thị đối tượng

Trước tiên, bạn cần chắc chắn rằng những đối tượng này có thực sự là thứ mà bạn muốn tuần tự hóa hay không. Bạn không cần phải tuần tự hóa chúng nếu các trường hợp của các đối tượng này xuất hiện trên đích.

Bây giờ bạn phát hiện ra rằng bạn cần phải tuần tự hóa đối tượng được trỏ bởi một con trỏ. Vấn đề của con trỏ rằng chúng chỉ hợp lệ trong chương trình sử dụng chúng. Bạn không thể tuần tự hóa con trỏ, bạn nên ngừng sử dụng chúng trong các đối tượng. Thay vào đó, hãy tạo các nhóm đối tượng. Nhóm đối tượng này về cơ bản là một mảng động chứa các "hộp". Các hộp này có số lượng tham chiếu. Số tham chiếu khác 0 cho biết một đối tượng đang hoạt động, 0 cho biết một vị trí trống. Sau đó, bạn tạo con trỏ thông minh tương tự như shared_ptr không lưu trữ con trỏ đến đối tượng, mà là chỉ mục trong mảng. Bạn cũng cần đồng ý về một chỉ mục biểu thị con trỏ null, ví dụ. -1.

Về cơ bản những gì chúng tôi đã làm ở đây là thay thế các con trỏ bằng các chỉ mục mảng. Bây giờ khi tuần tự hóa, bạn có thể tuần tự hóa chỉ mục mảng này như bình thường. Bạn không cần phải lo lắng về vị trí đối tượng sẽ ở trong bộ nhớ trên hệ thống đích. Chỉ cần đảm bảo rằng chúng cũng có cùng một nhóm đối tượng.

Vì vậy, chúng ta cần tuần tự hóa các nhóm đối tượng. Nhưng những cái nào? Khi bạn tuần tự hóa một đồ thị đối tượng, bạn không chỉ tuần tự hóa một đối tượng, bạn đang tuần tự hóa toàn bộ hệ thống. Điều này có nghĩa là việc tuần tự hóa hệ thống không nên bắt đầu từ các phần của hệ thống. Những đối tượng đó không nên lo lắng về phần còn lại của hệ thống, chúng chỉ cần tuần tự hóa các chỉ mục mảng và thế là xong. Bạn nên có một quy trình tuần tự hóa hệ thống sắp xếp việc tuần tự hóa hệ thống và đi qua các nhóm đối tượng liên quan và tuần tự hóa tất cả chúng.

Ở đầu nhận, tất cả các mảng chứa các đối tượng bên trong được giải hóa, tạo lại đồ thị đối tượng mong muốn.

Tuần tự hóa các con trỏ hàm

Không lưu trữ con trỏ trong đối tượng. Có một mảng tĩnh chứa các con trỏ đến các hàm này và lưu trữ chỉ mục trong đối tượng.

Vì cả hai chương trình đều có bảng này được biên dịch thành giá đỡ, nên chỉ sử dụng chỉ mục sẽ hoạt động.

Nối tiếp các kiểu đa hình

Vì tôi đã nói rằng bạn nên tránh các con trỏ trong các loại có thể tuần tự hóa và thay vào đó bạn nên sử dụng chỉ mục mảng, tính đa hình không thể hoạt động, vì nó yêu cầu con trỏ.

Bạn cần giải quyết vấn đề này với các thẻ loại và liên kết.

Phiên bản

Trên tất cả những điều trên. Bạn có thể muốn các phiên bản khác nhau của phần mềm tương thích với nhau.

Trong trường hợp này, mỗi đối tượng nên viết một số phiên bản vào đầu tuần tự của chúng để chỉ ra phiên bản.

Khi tải lên đối tượng ở phía bên kia, các đối tượng mới hơn có thể có thể xử lý các biểu diễn cũ hơn nhưng các đối tượng cũ hơn không thể xử lý mới hơn, vì vậy chúng nên đưa ra một ngoại lệ về điều này.

Mỗi khi có gì đó thay đổi, bạn nên tăng số phiên bản.


Vì vậy, để kết thúc điều này, tuần tự hóa có thể phức tạp. Nhưng may mắn thay, bạn không cần phải tuần tự hóa mọi thứ trong chương trình của mình, hầu hết chỉ các thông báo giao thức được tuần tự hóa, thường là các cấu trúc cũ đơn giản. Vì vậy, bạn không cần các thủ thuật phức tạp mà tôi đã đề cập ở trên quá thường xuyên.


1
Cảm ơn bạn. Câu trả lời này chứa một cái nhìn tổng quan tuyệt vời về các khái niệm liên quan đến việc tuần tự hóa dữ liệu có cấu trúc trong C ++.
Sean

0

Bằng cách học, tôi đã viết một bộ tuần tự C ++ 11 đơn giản. Tôi đã thử nhiều dịch vụ khác nặng ký hơn, nhưng muốn thứ gì đó mà tôi thực sự có thể hiểu được khi nó gặp sự cố hoặc không biên dịch được với g ++ mới nhất (điều này đã xảy ra với tôi với Cereal; một thư viện thực sự đẹp nhưng phức tạp và tôi không thể mò mẫm các lỗi mà trình biên dịch phát sinh khi nâng cấp.) Dù sao, nó chỉ có tiêu đề và xử lý các loại POD, vùng chứa, bản đồ, v.v. Không có phiên bản và nó sẽ chỉ tải các tệp từ cùng một vòm mà nó đã được lưu trong đó.

https://github.com/goblinhack/simple-c-plus-plus-serializer

Ví dụ sử dụng:

#include "c_plus_plus_serializer.h"

static void serialize (std::ofstream out)
{
    char a = 42;
    unsigned short b = 65535;
    int c = 123456;
    float d = std::numeric_limits<float>::max();
    double e = std::numeric_limits<double>::max();
    std::string f("hello");

    out << bits(a) << bits(b) << bits(c) << bits(d);
    out << bits(e) << bits(f);
}

static void deserialize (std::ifstream in)
{
    char a;
    unsigned short b;
    int c;
    float d;
    double e;
    std::string f;

    in >> bits(a) >> bits(b) >> bits(c) >> bits(d);
    in >> bits(e) >> bits(f);
}

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.