c++ custom object serialization

C ++에서의 직렬화 구현

 , 2012 년 11 월 26 일
C ++ 직렬화 구현

이메일 주소는 괜찮습니까? 귀하는 뉴스 레터에 가입했으나 귀하의 이메일 주소는 확인되지 않았거나 오랜 기간 재확인되지 않았습니다. 제발 클릭 여기에 확인 이메일이 전송 가지고 우리가 당신의 이메일 주소를 확인하고 다시 당신에게 뉴스 레터를 보내 시작할 수 있습니다. 또는 구독을 업데이트 할 수 있습니다 .
우리는 우리의 C ++ 객체가 영속성을 유지하기를 바랍니다.
모든 객체는 ‘상태’를 파일에 저장하고 프로세스가 시작될 때 읽는 방법을 알아야하므로 중단 된 부분부터 계속할 수 있습니다.
그러나 C ++의 직렬화 구현은 (C #과 달리) 사소한 것이 아니며 우리 모두가 의존하기를 원하지 않습니다
MFC 프레임 워크 또는 부스트 직렬화 (어떤 템플릿을 사용하여 거대한 실행 파일을 만들 수 있습니다!).
다음은 직렬화 구현에 대한 요점입니다.
1. 복제 가능한 객체에 대한 추상 클래스가 있어야합니다.
2. 클래스 이름을 가진 복제 가능한 객체를 만드는 팩토리 클래스가 있어야합니다.
(힌트 : 싱글 톤 구현 사용).
3. 템플릿을 전혀 사용할 필요가 없습니다!
4. C ++에 정적 생성자가 없기 때문에 클래스를 자동으로 factory에 등록하는 것은 쉽지 않습니다.
이 작업은 클래스에 정적 멤버를 추가하는 매크로로 수행 할 수 있습니다.
5. 객체를 어디에 저장했는지 모르기 때문에 추상 Archive 클래스가 있어야합니다.
(파일, 파이프, 메모리 ..).
6. auto_ptr을 사용하여 직렬화가 실패하거나 예외가 발생할 경우 오브젝트를 삭제하십시오.
다음은 직렬화를위한 간단하지만 완전한 구현 프레임 워크입니다 . 파일 설명 :

  • Dynamics.h – 복제 가능한 컬렉션의 싱글 톤 구현.
  • Persistent.h / .cpp – Dynamics.h를 사용하는 직렬화 구현.

Dynamics.h :
먼저 복제 가능한 모든 객체에 대한 기본 클래스를 선언합니다. 모든 복제 가능한 클래스는 ‘Clonable’에서 파생되고 createObj메소드를 구현해야합니다 . createObj클래스의 새 객체를 반환해야합니다.

class Clonable
{
public:
    virtual ~Clonable() {}
    virtual Clonable* createObj() const = 0;
};

복제 가능한 객체의 컬렉션을 선언해야합니다. 이름을 주면 모든 복제 가능한 클래스의 인스턴스를 만들 수 있어야합니다.
예 :

string className = "MyComplexClass";
Clonable* instance = Clonables::Instance().create(className);
MyComplexClass* pCmplx = dynamic_cast<MyComplexClass*>(instance);

아래는 Cloneable 컬렉션 클래스입니다. 싱글 톤 구현에 주목하십시오.

class Clonables {
private:
    typedef map<string, const Clonable*> NameToClonable;
    NameToClonable __clonables;
private:
    Clonables() {}
    Clonables(const Clonables&);                 // Prevent copy-construction
    Clonables& operator=(const Clonables&);      //  Prevent assignment
    ~Clonables()
    {
        for(NameToClonable::const_iterator it = __clonables.begin(); it != __clonables.end(); it++){
            const Clonable* clone = it->second;
            delete clone;
        }
        __clonables.clear();
    }
public:
    static Clonables& Instance()
    {
        static Clonables instance;   // Guaranteed to be destroyed.                              
        return instance;    // Instantiated on first use.
    }
public:
    void addClonable(const char* className, const Clonable* clone)
    {
        string name = className;
        NameToClonable::const_iterator it = __clonables.find(name);
        if(it == __clonables.end()) {
            __clonables[name] = clone;
        }
    }
    Clonable* create(const char *className)
    {
        string name = className;
        NameToClonable::const_iterator it = __clonables.find(name);
        if(it == __clonables.end()) return NULL;
        const Clonable* clone = it->second;
        return clone->createObj();
    }
};

'<code>Clonables복제 가능한 파생 클래스는 ‘(우리의 복제 가능한 컬렉션)에 등록 할 수 있도록 다음 클래스의 정적 멤버를 추가 할 수 있습니다 .

class AddClonable {
public:
    AddClonable(const char* className, const Clonable* clone){
        Clonables::Instance().addClonable(className, clone);
    }
};

Persist.h :
우리는 우리의 끈질긴 사물이 어디에 저장 될지 확신하지 못합니다. 따라서 우리는 스트리밍을위한 기본 클래스 (저장 및로드 지원 포함)를 구현해야합니다. 아카이브는 파일이나 파이프 또는 객체를 저장하는 모든 것이 될 수 있습니다.

class Archive
{
private:
    bool _isStoring;
public:
    Archive(bool isStoring = true) : _isStoring(isStoring) {}
    virtual ~Archive() {}
    virtual void write(const void* buffer, size_t length) {}
    virtual void read(void* buffer, size_t length) {}
    Archive& operator<<(const string& str);
    Archive& operator>>(string& str);
    Archive& operator<<(int val);
    Archive& operator>>(int& val);
    bool isStoring() const { return _isStoring; }
    void setDirection(bool isStoring) { _isStoring = isStoring; }
};

STL iostream을 사용하는 특정 ‘Archive’클래스를 정의합시다.

class ArchiveFile: public Archive
{
private:
    iostream* _stream;
public:
    ArchiveFile(iostream* stream) : _stream(stream) {}
    virtual ~ArchiveFile() {}
    virtual void write(const void *buffer, size_t length);
    virtual void read (void* buffer, size_t length);
};

Persistent 클래스는 아래의 ‘Persistent’클래스에서 파생되고 ‘serialize’메소드를 구현합니다.
영속 객체는 또한 복제 가능 객체입니다.

class Persistent : public Clonable
{
public:
    virtual ~Persistent() {}
    static Persistent* load(Archive& stream);
    void store(Archive& stream) const;
protected:
    virtual void serialize(Archive& stream) {}
    virtual int version() const { return 0; }
};

우리는이 방법의 자동 구현 createObj과 자동 등록을 원합니다.
우리 클래스의 복제물 컬렉션에. 이것은 다음의 매크로 declerations와 함께 할 수 있습니다 :
PERSISTENT_DECL매크로 createObj는 우리를위한 ‘Clonable’클래스 의 메소드를 구현합니다 . 또한 AddClonable클래스에 정적 멤버를 추가합니다 . 이렇게하면 우리의 영속 클래스가 복제 가능한 컬렉션에 자체 등록됩니다. 이것은 .h 클래스 정의에 추가되어야합니다 (아래 예 참조).

#define PERSISTENT_DECL(className) \
public: \
virtual Clonable* createObj() const \
{ \
    return new className(); \
} \
private: \
static AddClonable _addClonable;

PERSISTENT_IMPL이 정적 멤버를 초기화합니다. 이것은 .cpp 클래스 구현에 추가되어야합니다.

#define PERSISTENT_IMPL(className) \
    AddClonable className::_addClonable(#className, new className());

사용법의 예 : Event.h는 'Event' 영속성이 있어야 하는 간단한 클래스를 정의합니다 .

class Event : public Persistent {
private:
 int _id;
public:
 Event() : _id(0) {}
 virtual ~Event() {}
 int getId() const { return _id; }
protected:
 virtual void serialize(Archive& stream)
 {
  if(stream.isStoring())
   stream << _id;
  else
   stream >> _id;
 }
 PERSISTENT_DECL(Event)
};

Event.cpp

#include "Event.h"

PERSISTENT_IMPL(Event)

우리는에 뛰어 전에 '<code>Archive' 및 '<code>Persistent' 클래스 구현, 여기에 동일한 내용을 가진 새로운 개체로 다시 읽어 후 바탕 화면에 바이너리 파일로 우리의 ‘이벤트’객체를 직렬화하는 방법의 예입니다.

void serialize_example()
{
    auto_ptr<Event> event(new Event());
    fstream file("C:\\Users\\Gilad\\Desktop\\try.data",
        ios::out | ios::in | ios::binary | ios::trunc);
    ArchiveFile stream(&file);
    if(! file)
        throw "Unable to open file for writing";
    event->store(stream);
    file.seekg(0, ios::beg);
    Event* newEvent = dynamic_cast<Event*>(Persistent::load(stream));
    event.reset(newEvent);
    file.close();
}

<event>Persistent.cpp :
우리는 기본적인 ‘int’및 ‘string’아카이브를 사용하여 구현을 시작합니다.

Archive& Archive::operator<<(int val)
{
     write(&val, sizeof(int));
     return *this;
}
Archive& Archive::operator>>(int& val)
{
     read(&val, sizeof(int));
     return *this;
}
Archive& Archive::operator<<(const string& str)
{
    int length = str.length();
    *this << length;
    write(str.c_str(), sizeof(char) * length);
    return *this;
}
Archive& Archive::operator>>(string& str)
{
    int length = -1;
    *this >> length;
    vector<char> mem(length + 1);
    char* pChars = &mem[0];
    read(pChars, sizeof(char) * length);
    mem[length] = NULL;
    str = pChars;
    return *this;
}

이제 특정 STL iostream 보관 구현을 추가해 보겠습니다.

void ArchiveFile::write(const void* buffer, size_t length)
{
    _stream->write((const char*)buffer,length);
    if(! *_stream)
        throw "ArchiveFile::write Error";
}
void ArchiveFile::read(void* buffer, size_t length)
{
    _stream->read((char*)buffer, length);
    if(! *_stream)
        throw "ArchiveFile::read Error";
}

이것은 영구 객체를 아카이브에 저장 하는 방법입니다 .

  1. 개체의 클래스 이름을 저장하십시오.
  2. 클래스의 버전을 저장하십시오.
  3. 객체를 직렬화하도록 호출합니다.
void Persistent::store(Archive& stream) const
{
    string className = typeid(*this).name();
    className = className.substr(className.find(' ') + 1);
    stream << className;
    int ver = version();
    stream << ver;
    stream.setDirection(true);
    const_cast<Persistent *>(this)->serialize(stream);
}

다음은 아카이브에서 객체를 로드 하는 방법입니다 .

  1. 아카이브에서 클래스 이름 읽기
  2. cloneable 컬렉션과 간단한 캐스트를 사용하여 객체를 만듭니다.
  3. 버전이 올바른지 확인하십시오.
  4. 우리의 객체가 스스로를 비 직렬화하게하십시오.

auto_ptr의 사용에주의하십시오. (serialize 메소드와 같이) 예외가 발생하면 우리의 영속 객체가 삭제 될 것입니다.

Persistent* Persistent::load(Archive& stream)
{
    string className;
    stream >> className;
    Clonable* clone = Clonables::Instance().create(className.c_str());
    if(clone == NULL)
        throw "Persistent::load : Error creating object";
    auto_ptr<Clonable> delitor(clone);
    Persistent * obj = dynamic_cast<Persistent *>(clone);
    if(obj == NULL) {
        throw "Persistent::load : Error creating object";
    }
    int ver = -1;
    stream >> ver;
    if(ver != obj->version())
        throw "Persistent::load : unmatched version number";
    stream.setDirection(false);
    obj->serialize(stream);
    delitor.release();
    return obj;
}

C++ object serialization 란 무엇인가?

C++ object serialization 란 무엇인가?
국내에 번역된 말로는 “객체 직렬화” 라고 하는데, 우리나라말로 좀 더 풀어 보면, 객체의 메모리를 연속적인 바이트로 만들고, 만들어진 연속적인 바이트를 원래의 객체로 복원하는 작업을 말한다.
그러므로 C++ object serialize 라고 한다면, 위에서 말한 작업을 하라는 것이다.
어디에 쓰이는가?
이러한 객체 직렬화는 메모리에 있는 데이터를 스트림으로 보낼 때 사용 한다. 스트림을 이용하면 객체(객체를 아니여도 … )를 파일에/로/ 출력/입력 할 수 있으며, 네트워크에서 송수신 할 수 있으므로, 보통, 객체를 파일로 저장해서 읽으려고 할 때, 네트워크로 보내고 받을 때 이다.
개인적 사견으로는 암호화를 할 때, 사용 될 수도 있을것 같다.
여기서 잠깐 스트림에 대해서 정리하면, 스트림이란 입구와 출구가 있는 파이프 라인이다.
C++에서 어떻게 파일 스트림으로 보낼 수 있는가?
C++ 코드로 스트림화 한다면, 다음과 같을 것이다. 해당 소스 코드는
http://functionx.com/cpp/articles/serialization.htm
에서 가져 온 것이다.
츨력 예제.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <fstream>
#include <iostream>
using namespace std;
class Student
{
public:  
    char   FullName[40];  
    char   CompleteAddress[120];  
    char   Gender;  
    double Age;  
    bool   LivesInASingleParentHome;
};
int main()
{  
    Student one;  
    strcpy(one.FullName, "Ernestine Waller");  
    strcpy(one.CompleteAddress, "824 Larson Drv, Silver Spring, MD 20910");  
   
    one.Gender = 'F';  
    one.Age = 16.50;  
    one.LivesInASingleParentHome = true;      
    // output 파일 스트림을 열고
    ofstream ofs("fifthgrade.ros", ios::binary);  
   
    // 그 스트림에 Student 객체를 밀어 넣는다.
    ofs.write((char *)&one, sizeof(one));  
    return 0;
}

입력 예제

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <fstream>
#include <iostream>
using namespace std;
class Student
{
public:  
    char   FullName[40];  
    char   CompleteAddress[120];  
    char   Gender;  
    double Age;  
   
    bool   LivesInASingleParentHome;
};
int main()
{
    Student two;  
   
    // Input 파일 스트림을 열고
    ifstream ifs("fifthgrade.ros", ios::binary);  
    // 읽은 후
    ifs.read((char *)&two, sizeof(two));  
    // 출력해 본다.
    cout << "Student Information\n";  
    cout << "Student Name: " << two.FullName << endl;  
    cout << "Address:      " << two.CompleteAddress << endl;  
   
    if( two.Gender == 'f' || two.Gender == 'F' )      
        cout << "Gender:       Female" << endl;  
    else if( two.Gender == 'm' || two.Gender == 'M' )      
        cout << "Gender:       Male" << endl;  
    else
        cout << "Gender:       Unknown" << endl;  
   
    cout << "Age:          " << two.Age << endl;  
    if( two.LivesInASingleParentHome == true )      
        cout << "Lives in a single parent home" << endl;  
    else
        cout << "Doesn't live in a single parent home" << endl;      
    cout << "\n";  
    return 0;
}

 
직렬화가 보다 직관적이다 라고 해야 할까? 그냥 메모리에 있는것을 크기만큼 write 하면 끝나고 read 하면 끝난다. 하지만 이것이 쉬운 이유는, 객체가 POD 데이터이므로 쉬웠던 것이다.
실제로 객체의 안에 컨테이너가 있거나, 동적 메모리가 올라간 형태라면 위의 방식 데로 사용 할 수 없다. 왜냐하면 포인터를 예로 들면, 포인터 4 바이트는 특정 메모리를 가리키고 있는데, 특정 메모리까지 쫒아가 write 작업을 하지 않으면 말짱 꽝이기 때문이다.
즉, 쭉정이가 된다.(껍질만 있는 곡식이나 과일을 뜻한다.)
그래서 C++ 에서 객체를 다루고자 할 때는 손이 많이 간다. 그래서 그런지 이러한 작업을 도와 주는 라이브러리가 있다.
도와주는 라이브러리는 어떤게 있는가?

  1. Google Protocol Buffer
  2. Sweet Persist
  3. s11n
  4. boost::serialize

1. Google Protocol Buffer
독자적인 스크립트 언어가 있으며, 구글에서 만들었다. 객체마다 개별적인 스크립트를 만들어야 하는 불편함과, 한 종의 객체를 여러개 연결하여 하나의 파일로 출력 할 때는, 수동으로 그 객체의 경계를 만들어 주고 읽을 때 역시 그 곙계를 끊어서 읽어줘야 한다.
스크립트로 작성된 파일은 자바, C++, 파이선 등에서 읽고 쓸 수 있도록 개별 언어마다 코드를 만들어 준다. 그러므로, .. 다른 언어간의 이식성이 무척 뛰어나다.
만약 C++ 로 쓰고, 자바로 읽어야 되는 상황이나 그 반대의 상황일 때는 1번이 좋을 것이다.
2. Sweet Persist
라이렌스가 있어, 제약이 있어 알아보지 않았다. (사용해 보지 못했다.)
3. s11n
유니코드 미지원, 바이너리 출력을 할 수 없다. 그리고 정적라이브러리만 사용 할 수 있다. 하지만, 다양한 데이터 포맷(텍스트 기반)과 DB에 쓸 수 있도록 도와 준다. (사용해 보지 못했다)
4. boost::serialize
C++에서 사용하기 쉽다. 상속 객체, 멤버 객체, 배열, 표준 컨테이너, 표준 string 객체 등을 쉽게 읽고 쓸수 있게 해주며, 텍스트, XML, 바이너리, 유니코드 등을 지원한다. : )
테스트로 사용해 보는 중이고, 이번 스터디에서 소개가 될 라이브러리 이다.
현재인 2009-10-13짜의 정보이므로, 시간이 지나면, 변경될 수 있는 점이 있으니, 한번 직접 보길 바란다.
라이브러리를 어떻게 쓰는가?
boost:serialize의 사용법 : 바이너리로 std::vector<std::vector<unsigned char>> 를 밀어 넣고 빼오기만을 만들어 보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#include <iostream> // 화면에 보여 주기 위해
#include <fstream> // 파일 입출력 하기 위해
#include <boost/shared_ptr.hpp> // 최적화를 위해
#include <boost/foreach.hpp> // foreach 는 이제 지겹다.
#include <boost/archive/binary_iarchive.hpp> // 바이너리 형태로 입력하기 위해
#include <boost/archive/binary_oarchive.hpp> // 바이너리 형태로 출력하기 위해
#include <boost/serialization/vector.hpp> // 직렬화 vector를 사용하기 위해
template <typename ELEMENT, template <typename T, typename ALLOC = std::allocator<T> > class CON>
class fixel_table
{
public:
    typedef CON<ELEMENT> table_col;
    typedef typename CON<table_col>::iterator iterator;
    typedef typename CON<table_col>::const_iterator const_iterator;
public:
    explicit fixel_table( size_t col, size_t row, ELEMENT e )
    {
        table_col d( col, e ); // 열을 만듬
        table_.resize( row, d ); // 만든 열로, 행을 만듬
    }
    fixel_table()
    {
    }
    iterator begin()
    {
        return table_.begin();
    }
    iterator end()
    {
        return table_.end();
    }
    const iterator begin() const
    {
        return table_.begin();
    }
    const iterator end() const
    {
        return table_.end();
    }
    table_col& operator[]( size_t y )
    {
        return table_[y];
    }
private:
    friend class boost::serialization::access;
    friend std::ostream & operator<<( std::ostream &os, const fixel_table & t);
    template <typename Archive>
    void serialize( Archive &ar, const unsigned int version )
    {
        ar & table_;
    }
    
private:
    CON< table_col > table_;
};
typedef boost::shared_ptr< fixel_table<unsigned char, std::vector> > shp_fixel_table;
shp_fixel_table new_fixel_table( size_t x, size_t y )
{
    return shp_fixel_table( new fixel_table<unsigned char, std::vector>( x , y , 'O') );
}
// ... 결국 unsigned char 를 쓰긴 했으나, 좀더 하면 요소로 뺄 수 있음
// 귀찮아서 ...
void set_fixel_table( shp_fixel_table t, size_t x, size_t y, unsigned char color )
{       
    ((*t)[y])[x] = color;
}
void print_fixel_table( shp_fixel_table t )
{
    typedef shp_fixel_table::value_type::table_col table_col;
    typedef table_col::value_type value_type;
    BOOST_FOREACH( table_col & col, *t )
    {
        BOOST_FOREACH( value_type v , col )
        {
            std::cout << v << " ";
        }
        std::cout << std::endl;
    }
}   
void save_fixel_table( shp_fixel_table t, char * filename )
{
    std::ofstream ofs( filename, std::ios_base::binary );
    boost::archive::binary_oarchive oa( ofs );
    oa << *t;
}
void restore_fixel_table( shp_fixel_table t, char * filename )
{
    std::ifstream ofs( filename, std::ios_base::binary );
    boost::archive::binary_iarchive ia( ofs );
    ia >> *t;
}
int main( void )
{   
    // 1. 픽셀 테이블을 생성하여 값을 셋팅한다
    shp_fixel_table t = new_fixel_table( 10, 10 );
    set_fixel_table( t, 4, 3, 'R' );
    // 2. 출력 하고
    print_fixel_table( t );
    // 3. 저장 한다.
    save_fixel_table( t, "test" );
    std::cout << std::endl;
    // 4. 새로운 픽셀 테이블을 만들고
    shp_fixel_table new_t = new_fixel_table(0, 0);
    restore_fixel_table( new_t, "test" );
    // 5. 잘 되었는지 확인해 본다.
    print_fixel_table( new_t );
    return 0;
}

… 익히는게 어렵지 않고, 샘플 코드도 많이 제공 하고 있으니, 영어를 못한다고 해도 크게 무리가 되진 않는다.
boost::serialize 에서 어떻게 바이너리인 메모리 데이터가 문자로 바뀔 수 있을까?
<정리중>
boost::serialize 에서 직렬된 바이너리/텍스트 의 크기가 얼마나 되는지 어떻게 알 수 있을까?
<정리중>
boost::serialize 에서 직렬 시킬 대상을 파일/스트링을 제외하고, 메모리에 어떻게 바로 저장 할 수 있을까요?