Knowhow/C, C++, CMake

Boost serialization

침닦는수건 2023. 9. 25. 20:09
반응형

c++에서 새로운 클래스를 정의하고 이로 만든 객체를 저장하고 싶을 경우가 있다. 나는 대표적으로 Frame이라는 클래스 내에 이미지, 이미지 경로, 이미지 feature, intrinsic parameters 등 전부 담아둔 뒤, 모든 입력 이미지를 Frame 객체로 만들어두는 작업에서 필요성을 느꼈다. 

 

이 경우 저장을 어떻게 할지 멤버 변수마다 하나하나 지정해줘서 저장 함수를 구현해야 하기 마련인데 이게 도통 귀찮은 일이 아니기 때문에 이를 지원하는 기능이 boost에 있었다. 

 

boost의 serialization 기능을 이용해 객체 자체를 직렬화해버리고 그냥 파일에 써버리는 것이다. 당연히 human readable 형태는 아니지만 deserialization 기능을 이용해 다시 원복했을 때 저장했던 객체를 그대로 받을 수 있다. 

 

이 기능을 이용하기 위해선 클래스 내/외에 특별히 구현해주어야 하는 함수들이 있는데 이에 대해 간단히 정리해보고자 한다.

 

크게 방법은, 두가지가 있다. 

1) class 내부에 구현하는 방법

2) class 외부에서 boost:serialization 자체에 overriding해서 박아넣는 방법

 

어떤 방법이 선호되는지는 모르겠으나, 코드 리뷰 경험으로 보면 직접 구현한 클래스를 쓴다면 1)로 하는 것 같고 나머지는 2)의 방식을 쓰는 듯하다. 

 

1) class 내부에 구현하는 방법

먼저 참고하고 가면 좋은 글이 있어 첨부한다. 그림을 포함해서 간단한 개념을 정리하기 좋은 글.

 

https://object-world.tistory.com/10

 

Boost를 이용한 객체 직렬화(Object Serialization)

객체 직렬화 객체 직렬화는 객체를 전송가능한 형태의 연속적인 데이터로 변형하는 것으로 파일로 저장하거나 전송할 때 사용된다. 객체를 연속적인 데이터로 변환하는 과정을 직렬화 연속적

object-world.tistory.com

위 글에서는 boost::archive::text_oarchive를 주로 사용했지만 실제로 열어볼 것이 아닌 경우가 많기 때문에, boost::archive::binary_oarchive도 많이 쓴다.

 

  // p : boost:filesystem.path
  
  std::ofstream ofs(p.string(), std::ios_base::out | std::ios_base::trunc | std::ios::binary);
  boost::archive::binary_oarchive oa(ofs);

이렇게 쓰면 된다. 

 

내용을 다시 짚어보면, class 내부에 다음 함수를 추가해줘야 한다.

private:
    friend class boost::serialization::access;
    
    template<class Archive>
    void serialize(Archive& ar, const unsigned int version){
    	ar & member1;
        ar & member2;
    }

boost::serialization::access을 friend로 포함시켜줌으로써 외부에서 접근할 수 있도록 열어주는 것이 기본이고 이후에 serialize라는 함수를 overriding해주어야 한다. 

 

여기서 저 member1, member2가 그냥 int, float 같은 기본형이면 이걸로 끝이지만 만약 또 다른 class, 예를 들어 cv나 Eigen 형일 경우, cv, Eigen에 대해서도 각각 serialization overriding을 해주어야 한다. 그 방법은 2)에 해당하므로 뒤에서 설명하겠다. 

 

일단 위와 같이 박아넣어주면 boost::archive::binary_oarchive << 을 이용해 클래스를 저장할 수 있게 된다.

 

여기서 간혹 코드를 보면 serialize를 overriding 해주는 대신 save와 load 2개를 각각 써주는 경우도 있는데, 이 방식은 serialize를 2개로 쪼갠 형태일 뿐 동일하다. 왜 쓰냐면 serialize에서 저장한 것을 그대로 로딩하는 경우가 아니라 저장할 때랑 로딩할 때 약간의 차이가 있을 때 쓴다. 예를 들어 저장할 때는 멤버변수 3개를 저장하고 읽을 때는 2개만 읽는 식이다. (권장되는 방식은 아니라고 한다. 근데 자주 쓴다. 이미지 저장할 땐 cv::imencode 써야하는데 불러올 땐 cv::imdecode를 써야하니까.)

 

private:
    friend class boost::serialization::access;
    
    template<class Archive>
    void save(Archive& ar, const unsigned int version){
    	ar << member1;
        ar << member2;
        std::vector<uchar> buf;
        cv::imencode(".jpg", m_img, buf);
        ar << buf;
    }
    
    template<class Archive>
    void load(Archive& ar, const unsigned int version){
    	ar >> member1;
        ar >> member2;
        std::vector<uchar> buf;
        ar >> buf;
        m_img = cv::imdecode(buf, cv::IMREAD_ANYCOLOR);
    }
    
    template<class Archive>
    void serialize(Archive& ar, const unsigned int version){
    	boost::serialization::split_member(ar, *this, version);
    }

예를 들면 위와 같다. 권장되진 않으나 save, load 형태가 훨씬 많이 쓰이는 것 같다.  serialize는 여전히 정의해주어야 하는데 안에 내용이 간단해진다. 

 

2)에서 쓰는 BOOST_SERIALIZATION_SPLIT_???를 사용할 수도 있다는데 방법 잘 모르겠다. class 내부에서 저걸 쓸 때는 ???에 MEMBER를, 외부에서 쓸 때는 FREE를 넣어주면 되는데 자세한 건 더 검색해보길 바람.

 

2) class 외부에서 boost:serialization 자체에 박아넣는 방법

결국 1)에서 access 넣어주고 overriding 해주고 이런 일련의 과정이 하고자 하는 것은 boost::serialization이 이 class.serialize()를 호출할 수 있게 하기 위함이다. 

 

그런데 더 간단하게 만일 boost::serialization자체에서 새로운 형을 직접 커버할 수 있도록 overriding해줄 수도 있다. 

 

이 경우 문법이 다음과 같다. boost::serialization::seiralize 직접 overriding해준다.

namespace boost {
namespace serialization{
template <class Archive> void serialize(Archive& ar, Eigen::Matrix4d& mat, const unsigned int verion){
	ar & make_array(mat.data(), mat.size());
}
}
}

 

save, load로 나눠서 overriding해주는 것도 가능한데 하나가 추가된다. BOOST_SERIALIZATION_SPLIT_FREE(typename)이 namespace "바깥"에서 선언되어야 한다. 

BOOST_SERIALIZATION_SPLIT_FREE(Eigen::Matrix4d)
namespace boost {
namespace serialization {
template <class Archive>
void save(Archive& ar, const Eigen::Matrix4d& mat, const unsigned int version) {
  int rows = mat.rows();
  int cols = mat.cols();

  ar & rows;
  ar & cols;
  ar & make_array(mat.data(), rows * cols);
}

template <class Archive>
void load(Archive& ar, Eigen::Matrix4d& mat, const unsigned int version) {
  int rows, cols;
  ar & rows;
  ar & cols;
  mat.resize(rows, cols);
  ar & make_array(mat.data(), rows * cols);
}
} 
}

 

2)와 같은 방식은 serialization.hpp 같은 파일 따로 하나 파서 사용할 예정인 모든 형을 주루룩 다 정의해놓고 include해서 쓰는게 편하다. 

 

새로 정의하는 class를 여기다가 구현할 경우 너무 길어지니까 그건 1)의 방식대로 내부에 friend + @를 이용해 구현하고 간단히 cv, eigen 같은 것들은 전부 구현해서 몰아놓는 것이 좋다. 

 

 

정식 문서

https://www.boost.org/doc/libs/1_83_0/libs/serialization/doc/serialization.html

 

Serialization - Serialization of Classes

Serialization Serializable Concept Primitive Types Class Types Member Function Free Function Namespaces for Free Function Overrides Class Members Base Classes const Members Templates Versioning Splitting serialize into save/load Member Functions Free Funct

www.boost.org

 

반응형

'Knowhow > C, C++, CMake' 카테고리의 다른 글

TBB 팁  (0) 2023.09.25
Boost serialization 팁들  (0) 2023.09.25
CMake 백과사전  (0) 2023.09.21
VScode C/C++ 개발 세팅, CMakeLists.txt 이용하기 2  (0) 2023.01.09
VScode C/C++ 개발 세팅, CMakeLists.txt 이용하기 1  (0) 2023.01.09