「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 來讀取。這樣就可以正確地把 sData 和 iValue 的資料讀取回來了~
這邊稍微要注意的是,在使用 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; } } }
如此一來,就可以在不修改類別的情況下,讓他支援序列化了~
基礎使用的部分大致上就先這樣,下一篇再來紀錄一些比較細的東西。
參考: