儲存 C++ 的類別資料:Boost Serialization(part 1)

| | 0 Comments| 09:55
Categories:

「Serialization」(序列化、維基百科)在程式語言裡面,基本上是一種用來把資料結構或是物件的狀態,轉換成可儲存、可交換的格式的一種方法。
透過這樣的功能,可以把物件的狀態儲存下、之後再還原回來。

XML、JSON 這類的格式,實際上都可以算是序列化資料格式的一種。

而在 C++ 的標準函式庫中,並沒有提供相關的功能,所以這邊 Heresy 是找 Boost C++ Libraries 的 Boost.Serialization 來用了。

他的官方文件是:https://www.boost.org/libs/serialization/doc/index.html

要使用 Boost Serialization 算是滿簡單的,主要是兩個步驟:

  • 讓要使用的資料可以序列化
  • 選擇要使用的封存(archive)方式

可序列化(Serializable)的資料

可以使用 Boost serialization 進行序列化處理的資料,包括了:

  • C++ 原生型別(primitive type)
  • 有定義 serialize() 這個成員函式的類別(class)
    • 或是有對應的全域函式也可以
  • 可序列化型別的指標、參考、C++ 原生陣列

而針對標準函式庫的型別,Boost serialization 大多也都可以支援。

所以,如果是自己定義的型別的話,在最簡單的狀況下,只要加入一個 serialize() 函式,就可以支援 Boost serialization 了~


封存用的 Archive

在儲存方式的部分,Boost 有提供 text、binary、XML 三種 archive 可以使用,一般來說應該算是夠用了。(比較可惜是沒原生支援 JSON)

他們的類別分別是:

// a portable text archive
boost::archive::text_oarchive; // saving
boost::archive::text_iarchive; // loading
// a portable text archive using a wide character stream
boost::archive::text_woarchive; // saving
boost::archive::text_wiarchive; // loading
// a portable XML archive
boost::archive::xml_oarchive; // saving
boost::archive::xml_iarchive; // loading
// a portable XML archive which uses wide characters - use for utf-8 output
boost::archive::xml_woarchive; // saving
boost::archive::xml_wiarchive; // loading
// a non-portable native binary archive
boost::archive::binary_oarchive; // saving
boost::archive::binary_iarchive; // loading

在命名的規則上,也還算好理解。以 text_woarchive 來說,「text」就是格式、「w」則是代表寬字元,「o」代表是輸出、「i」則是輸入。

要使用時,都需要 include 對應的 header 檔;以「text_woarchive」來說,他的 header 檔就是「<boost/archive/text_woarchive.hpp>」。

而在操作上,archive 的類別基本上是把標準函式庫既有的 stream 作封包,在使用上就接近本來的 stream 一樣,可以透過 <<>> 來做輸出和輸入,相當地簡單。

不過這邊可能要注意的,是 binary 的 archive 基本上會受到平台實作的影響,所以在跨平台的時候可能會有問題。


簡單的範例

在使用上,Boost serialization 最簡單的範例,大概會是像下面這樣:

#include <string>
#include <fstream>
#include <iostream>
#include <boost/archive/text_iarchive.hpp>
#include <boost/archive/text_oarchive.hpp>
int main()
{
  {  // Save
    std::string sData = "Hello world";
    int iValue = 20;
    std::ofstream ofs( "data.txt" );
    {
      boost::archive::text_oarchive oa(ofs);
      oa << sData;
      oa << iValue;
    }
    ofs.close();
  }
  {  // Load
    std::string sData;
    int iValue;
    std::ifstream ifs("data.txt");
    {
      boost::archive::text_iarchive ia(ifs);
      ia >> sData;
      ia >> iValue;
    }
    ifs.close();
    std::cout << sData << iValue << std::endl;
  }
}

這邊基本上是使用最簡單的 text archive 來做資料的封存。

在第一個區塊,是把 sData 這個字串、以及 iValue 這個整數、透過 text_oarchive 來封存;而由於這邊的 text_oarchive 的物件 oa 實際上是把資料輸出到 ofs 這個 std::ofstream 的檔案流中,所以最後的資料會儲存在 data.txt 這個檔案裡。

根據上面的程式碼,最後 data.txt 的內容大致上會像下面這樣:

22 serialization::archive 17 11 Hello world 20

不過由於這邊是根據 text_oarchive 的實作而產生的資料,之後改版的話、可能也會有所變化,所以最好不要自己另外寫程式去分析它會比較好。

而在第二個區塊,則是先使用 std::ifstream 的物件 ifs 來開啟 data.txt 這個檔案,然後再透過 text_iarchive 的物件 ia 來讀取。這樣就可以正確地把 sDataiValue 的資料讀取回來了~

這邊稍微要注意的是,在使用 text archive 的時候,變數的輸入是有順序性的,如果儲存和讀取的順序不一致,是會產生例外狀況的。


使用 XML 格式來儲存

如果想要改用比較適合輸出給其他程式使用的格式的話,使用 XML archive 或許會是個比較合適的選擇。

他的基本使用方法和 text archive 大致相同,不過一個很大的不同,是他輸出時需要將資料封包成「Name-Value Pairs」(官方文件)的形式、以提供 XML 必要的標籤名稱。

要做到這件事,最簡單的方法,就是使用他提供的 BOOST_SERIALIZATION_NVP() 這個巨集。

上面的程式碼大致上可以修改成下面的樣子:

#include <string>
#include <fstream>
#include <iostream>
#include <boost/archive/xml_iarchive.hpp>
#include <boost/archive/xml_oarchive.hpp>
int main()
{
  {  // Save
    std::string sData = "Hello world";
    int iValue = 20;
    std::ofstream ofs( "data.xml" );
    {
      boost::archive::xml_oarchive oa(ofs);
      oa << BOOST_SERIALIZATION_NVP( sData );
      oa << BOOST_SERIALIZATION_NVP( iValue );
    }
    ofs.close();
  }
  {  // Load
    std::string sData;
    int iValue;
    std::ifstream ifs("data.xml");
    {
      boost::archive::xml_iarchive ia(ifs);
      ia >> BOOST_SERIALIZATION_NVP( sData );
      ia >> BOOST_SERIALIZATION_NVP( iValue );
    }
    ifs.close();
    std::cout << sData << iValue << std::endl;
  }
}

這樣儲存下來的格式,就會變成類似下面的形式:

<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<!DOCTYPE boost_serialization>
<boost_serialization signature="serialization::archive" version="17">
<sData>Hello world</sData>
<iValue>20</iValue>
</boost_serialization>

這邊可以看到,BOOST_SERIALIZATION_NVP() 會直接拿變數的名稱作為 XML 的標籤名稱;如果不想這樣做、而是想自己控制變數名稱的話,也可以改用 boost::serialization::make_nvp() 這個函式。

例如把上面的 BOOST_SERIALIZATION_NVP( sData ) 改成 boost::serialization::make_nvp( data, sData ) 的話,就可以將他的 XML 標籤名稱由 sData 改成 data 了。

而這邊個人覺得很遺憾的是,這邊雖然存成有名稱的 XML 變數了,但是他讀取的順序還是得和儲存時完全相同,沒辦法根據變數名稱來自己處理,算是有點浪費 XML 的功能的感覺。


讓自訂型別支援 Boost serialization

上面的例子,寫的都算是 C++ 本身的型別,而如果是自己定義的類別該怎麼辦呢?

就如同前面所提過的,在最簡單的狀況下,只需要撰寫一個名為 serialize() 的成員函式就可以了。

這邊就是一個簡單的例子:

class CURL
{
public:
  std::string   sHost;
  unsigned int  sPort;
  std::string   sPath;
public:
  template<class Archive>
  void serialize(Archive& ar, const unsigned int version)
  {
    ar& sHost;
    ar& sPort;
    ar& sPath;
  }
};

只要有定義這樣的 serialize() 函式,自定義的 CURL 這個類別,就可以像 C++ 原生型別一樣,拿來搭配 Boost Serialization 的 archive 使用、進行序列化了。

而在 Heresy 來看,Boost serialization 的實作概念很有趣。

首先,這邊是寫成 template 的形式,其中 Archive 可以是所有輸入輸出的 archive。

然後,他又透過 & 這個運算子來統一輸入(<<)以及輸入(>>)的介面;當 Archive 這個 template 型別屬於輸出的 Archive 的時候,他就會輸出資料、而當 Archive 是輸入的 archive 的時候,就會自動變成輸入。

所以,接下來只要把要儲存/讀取的資料(這邊是所有的成員資料),透過 & 這個運算子來給 Archive 的物件 ar 做處理就可以了。

這邊唯一的需求,就是這些資料必須要可以序列化了~如果遇到不能序列化的資料型別,就需要另外幫他撰寫 serialize() 這樣的函式了。

透過這樣的設計,一個類別序列化的輸入和輸出,可以用同一個函式解決掉,不需要分開撰寫;這樣的好處,除了程式碼的簡化之外,也可以避免輸入和輸出不一致而導致的問題。

另外,他還有一個參數是 version,這是用來紀錄物件的版本用的;透過他,就可以根據不同的版本、進行跨版本的檔案相容控制。不過這部分算是比較進階的東西,就等之後有機會再提吧。


讓既有的型別支援序列化

上面的方法,是在類別裡面加入 serialize() 這樣的函式,讓他支援 Boost Serialization。但是實際上,很多時候,並不一定可以修改類別的內容、也就是沒辦法加入這樣的成員函式。

為了對應這樣的狀況,Boost Serialization 也有提供「非侵入式」(non Intrusive)的方案可以使用。

要使用非侵入式的方案,基本上就是把 serialize() 這個函式,改寫成在 boost::serialization 這個 namespace 下的全域函式,這樣就可以了。

例如上面 CURL 的例子,就可以改成:

class CURL
{
public:
  std::string   sHost;
  unsigned int  sPort;
  std::string   sPath;
};
namespace boost {
  namespace serialization {
    template<class Archive>
    void serialize(Archive& ar, CURL& rURL, const unsigned int version)
    {
      ar& rURL.sHost;
      ar& rURL.sPort;
      ar& rURL.sPath;
    }
  }
}

如此一來,就可以在不修改類別的情況下,讓他支援序列化了~


基礎使用的部分大致上就先這樣,下一篇再來紀錄一些比較細的東西。


參考:

Leave a Reply

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *