C ++에서 객체를 어떻게 직렬화합니까?


84

소켓 연결을 통해 직렬화하고 전송해야하는 작은 개체 계층 구조가 있습니다. 객체를 직렬화 한 다음 유형에 따라 역 직렬화해야합니다. C ++에서이 작업을 수행하는 쉬운 방법이 있습니까 (Java에 있음)?

C ++ 직렬화 온라인 코드 샘플 또는 자습서가 있습니까?

편집 : 명확하게 말하면 객체를 바이트 배열로 변환 한 다음 다시 객체로 변환하는 방법을 찾고 있습니다. 소켓 전송을 처리 할 수 ​​있습니다.


3
google :: protobuf를 확인하세요 . 바이너리 직렬화를위한 매우 강력하고 빠른 라이브러리입니다. boost :: asio 등으로 성공적으로 사용했습니다.
Ketan

지속성 구현이있는 [STLPLUS] [1], lib를 살펴보십시오. [1] : stlplus.sourceforge.net
lsalamon

4
제공된 답변은 실제로 직렬화 하는 방법 을 설명하지 않습니다 . 하나는 부스트 직렬화 라이브러리를 제공하고 다른 하나는 순진한 구현에서 문제를 설명합니다. 이것은 C ++-faq 질문이므로 누군가 실제로 대답 할 수 있습니까?
익명

답변:


55

직렬화에 대해 이야기하면 부스트 직렬화 API 가 떠 오릅니다. 직렬화 된 데이터를 네트워크를 통해 전송하려면 Berkeley 소켓이나 asio 라이브러리를 사용 합니다.

편집 :
객체를 바이트 배열로 직렬화하려는 경우 다음과 같은 방식으로 부스트 직렬 변환기를 사용할 수 있습니다 (자습서 사이트에서 가져옴).

#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)
    {}
};

실제 직렬화는 매우 쉽습니다.

#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
    }

역 직렬화는 유사한 방식으로 작동합니다.

포인터의 직렬화 (tress 등과 같은 복잡한 데이터 구조는 문제 없음), 파생 클래스를 처리 할 수있는 메커니즘도 있으며 이진 및 텍스트 직렬화 중에서 선택할 수 있습니다. 게다가 모든 STL 컨테이너는 즉시 지원됩니다.


이것은 c ++ 질문입니다. gps_position 클래스가 << 연산자를 오버로딩하는 이유입니다. 정의 된 친구 기능이 없습니다
비센테 Bolea

"친구 클래스 boost :: serialization :: access"에 주목하십시오. 이렇게하면 비공개 인 경우에도 클래스 멤버에 직렬화 라이브러리 함수에 액세스 할 수 있습니다.
Robert Ramey

13

경우에 따라 단순 유형을 처리 할 때 다음을 수행 할 수 있습니다.

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

개념 증명 또는 초안으로 괜찮으므로 팀의 다른 구성원이 다른 부분에서 계속 작업 할 수 있습니다.

그러나 조만간, 일반적으로 조만간 이것은 당신을 다치게 할 것입니다!

다음과 같은 문제가 발생합니다.

  • 가상 포인터 테이블이 손상됩니다.
  • 포인터 (데이터 / 멤버 / 함수)가 손상됩니다.
  • 다른 기계에서 패딩 / 정렬의 차이.
  • Big / Little-Endian 바이트 순서 문제.
  • float / double 구현의 변형.

(또한 수신 측에서 무엇을 풀고 있는지 알아야합니다.)

모든 클래스에 대해 고유 한 마샬링 / 언 마샬링 메서드를 개발하여이를 개선 할 수 있습니다. (이상적으로는 가상이므로 하위 클래스에서 확장 할 수 있습니다.) 몇 가지 간단한 매크로를 사용하면 빅 / 리틀 엔디안 중립 순서로 다른 기본 유형을 매우 빠르게 작성할 수 있습니다.

그러나 그런 종류의 지저분한 작업은 boost의 직렬화 라이브러리 를 통해 훨씬 더 좋고 더 쉽게 처리됩니다 .


그게 제가 생각했던 것입니다. 그러나 네트워크 스트림으로 직렬화하고 싶기 때문에 전혀 작동하지 않습니다. 기껏해야 엔디안과 다른 플랫폼 때문입니다. 그러나 나는 그것이 가상 포인터를 손상 시킨다는 것을 몰랐습니다. 감사합니다 =)
Atmocreations

4

객체를 직렬화하는 데 사용할 수있는 일반적인 패턴이 있습니다. 기본 기본 요소는 반복기에서 읽고 쓸 수있는 다음 두 함수입니다.

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;
}

그런 다음 직렬화 및 역 직렬화 기능은 다음 패턴을 따릅니다.

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.
}

클래스의 경우 friend 함수 패턴을 사용하여 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.
};

프로그램에서 직렬화하고 다음과 같은 파일로 개체를 지정할 수 있습니다.

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

그런 다음 읽으십시오.

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

내 오래된 대답은 다음과 같습니다.

직렬화는 객체를 이진 데이터로 바꾸는 것을 의미합니다. 역 직렬화는 데이터에서 개체를 다시 만드는 것을 의미합니다.

직렬화 할 때 바이트를 uint8_t벡터 로 푸시합니다 . 직렬화를 해제 할 때 uint8_t벡터 에서 바이트를 읽는 것 입니다.

물건을 직렬화 할 때 사용할 수있는 패턴이 확실히 있습니다.

각 직렬화 가능 클래스 serialize(std::vector<uint8_t> &binaryData)에는 제공된 벡터에 이진 표현을 기록 하는 또는 유사한 서명 함수 가 있어야합니다 . 그런 다음이 함수는이 벡터를 멤버의 직렬화 함수로 전달하여 자신의 내용도 쓸 수 있습니다.

데이터 표현은 아키텍처마다 다를 수 있기 때문입니다. 데이터를 표현하는 방법을 찾아야합니다.

기본부터 시작하겠습니다.

정수 데이터 직렬화

리틀 엔디안 순서로 바이트를 작성하십시오. 또는 크기가 중요한 경우 varint 표현을 사용하십시오.

리틀 엔디안 순서로 직렬화 :

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

리틀 엔디안 순서에서 역 직렬화 :

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

부동 소수점 데이터 직렬화

내가 아는 한 IEEE 754는 여기에서 독점권을 가지고 있습니다. 나는 수레에 대해 다른 것을 사용할 주류 아키텍처를 알지 못합니다. 다를 수있는 유일한 것은 바이트 순서입니다. 일부 아키텍처는 리틀 엔디안을 사용하고 다른 아키텍처는 빅 엔디안 바이트 순서를 사용합니다. 이것은 수신 측에서 바이트를 크게하는 순서에주의해야 함을 의미합니다. 또 다른 차이점은 비정규 및 무한대 및 NAN 값을 처리하는 것입니다. 그러나 이러한 값을 피하는 한 괜찮습니다.

직렬화 :

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

역 직렬화는 역 직렬화입니다. 아키텍처의 바이트 순서에 유의하십시오!

문자열 직렬화

먼저 인코딩에 동의해야합니다. UTF-8이 일반적입니다. 그런 다음 길이 접두사 방식으로 저장합니다. 먼저 위에서 언급 한 방법을 사용하여 문자열의 길이를 저장 한 다음 문자열을 바이트 단위로 작성합니다.

배열 직렬화.

그들은 문자열과 동일합니다. 먼저 배열의 크기를 나타내는 정수를 직렬화 한 다음 배열의 각 개체를 직렬화합니다.

전체 객체 직렬화

앞서 말했듯 serialize이 벡터에 콘텐츠를 추가 하는 방법이 있어야합니다 . 객체를 직렬화 해제하려면 바이트 스트림을 사용하는 생성자가 있어야합니다. 일 수 istream있지만 가장 간단한 경우에는 참조 uint8_t포인터 일 수 있습니다 . 생성자는 스트림에서 원하는 바이트를 읽고 객체의 필드를 설정합니다. 시스템이 잘 설계되고 개체 필드 순서로 필드를 직렬화하는 경우 이니셜 라이저 목록의 필드 생성자에 스트림을 전달하고 올바른 순서로 역 직렬화 할 수 있습니다.

개체 그래프 직렬화

먼저 이러한 객체가 실제로 직렬화하려는 것인지 확인해야합니다. 이러한 개체의 인스턴스가 대상에있는 경우 직렬화 할 필요가 없습니다.

이제 포인터가 가리키는 객체를 직렬화해야한다는 것을 알았습니다. 포인터를 사용하는 프로그램에서만 유효하다는 문제. 포인터를 직렬화 할 수 없으므로 개체에서 포인터 사용을 중지해야합니다. 대신 개체 풀을 만듭니다. 이 개체 풀은 기본적으로 "상자"를 포함하는 동적 배열입니다. 이 상자에는 참조 횟수가 있습니다. 0이 아닌 참조 횟수는 라이브 객체를 나타내고 0은 빈 슬롯을 나타냅니다. 그런 다음 객체에 대한 포인터를 저장하지 않고 배열의 인덱스를 저장하는 shared_ptr과 유사한 스마트 포인터를 만듭니다. 또한 널 포인터를 나타내는 색인에 동의해야합니다. -1.

기본적으로 여기서 우리가 한 일은 포인터를 배열 인덱스로 대체 한 것입니다. 이제 직렬화 할 때 평소처럼이 배열 인덱스를 직렬화 할 수 있습니다. 대상 시스템의 메모리에서 개체가 어디에 있는지 걱정할 필요가 없습니다. 동일한 개체 풀이 있는지 확인하십시오.

따라서 개체 풀을 직렬화해야합니다. 그러나 어떤 것? 객체 그래프를 직렬화 할 때 객체를 직렬화하는 것이 아니라 전체 시스템을 직렬화하는 것입니다. 이것은 시스템의 직렬화가 시스템의 일부에서 시작되어서는 안된다는 것을 의미합니다. 이러한 개체는 시스템의 나머지 부분에 대해 걱정할 필요가 없으며 배열 인덱스를 직렬화하기 만하면됩니다. 시스템 직렬화를 오케스트레이션하고 관련 개체 풀을 살펴보고 모두 직렬화하는 시스템 직렬화 루틴이 있어야합니다.

수신 측에서 모든 배열과 개체가 역 직렬화되어 원하는 개체 그래프를 다시 만듭니다.

함수 포인터 직렬화

개체에 포인터를 저장하지 마십시오. 이러한 함수에 대한 포인터를 포함하는 정적 배열을 갖고 객체에 인덱스를 저장합니다.

두 프로그램 모두이 테이블이 쉘프에 컴파일되어 있으므로 인덱스 만 사용하면됩니다.

다형성 유형 직렬화

직렬화 가능한 유형의 포인터를 피해야하고 대신 배열 인덱스를 사용해야한다고 말 했으므로 다형성은 포인터가 필요하기 때문에 작동하지 않습니다.

유형 태그 및 공용체로이 문제를 해결해야합니다.

버전 관리

위의 모든 것 위에. 서로 다른 버전의 소프트웨어가 상호 운용되기를 원할 수 있습니다.

이 경우 각 개체는 직렬화 시작 부분에 버전 번호를 작성하여 버전을 표시해야합니다.

다른 쪽에서 객체를로드 할 때 최신 객체는 이전 표현을 처리 할 수 ​​있지만 이전 표현은 최신 표현을 처리 할 수 ​​없으므로 이에 대한 예외를 throw해야합니다.

무언가 변경 될 때마다 버전 번호를 올려야합니다.


그래서 이것을 마무리하기 위해 직렬화는 복잡 할 수 있습니다. 그러나 다행스럽게도 프로그램의 모든 것을 직렬화 할 필요는 없습니다. 대부분의 경우 프로토콜 메시지 만 직렬화되며, 이는 종종 평범한 오래된 구조체입니다. 따라서 위에서 언급 한 복잡한 트릭이 너무 자주 필요하지 않습니다.


1
감사합니다. 이 답변에는 C ++에서 구조화 된 데이터 직렬화와 관련된 개념에 대한 훌륭한 개요가 포함되어 있습니다.
Sean

0

학습을 통해 저는 간단한 C ++ 11 직렬 변환기를 작성했습니다. 나는 다른 다양한 무거운 제품을 시도해 보았지만 잘못되거나 최신 g ++로 컴파일하지 못했을 때 실제로 이해할 수있는 무언가를 원했습니다. (Cereal에서 저에게 일어났던 일입니다. 정말 멋진 라이브러리이지만 복잡하고 그럴 수 없었습니다. 컴파일러가 업그레이드 할 때 발생시킨 오류.) 어쨌든, 헤더 만 있고 POD 유형, 컨테이너, 맵 등을 처리합니다. 버전 관리가 없으며 저장된 동일한 아치에서 파일 만로드합니다.

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

사용 예 :

#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);
}

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.