C++ Boost

序列化

教學


一個非常簡單的案例
非侵入式版本
可序列化的成員
衍生類別
指標
陣列
STL 容器
類別版本控制
serialize 拆分為 save/load
存檔
範例列表
輸出存檔類似於輸出資料串流。可以使用 << 或 & 運算子將資料儲存到存檔中。

ar << data;
ar & data;
輸入存檔類似於輸入資料串流。可以使用 >> 或 & 運算子從存檔中載入資料。

ar >> data;
ar & data;

當這些運算子被用於基本資料類型時,資料會簡單地儲存/載入到存檔中/從存檔中。當用於類別資料類型時,會呼叫類別的 serialize 函式。每個 serialize 函式都使用上述運算子來儲存/載入其資料成員。這個過程將以遞迴方式繼續,直到類別中包含的所有資料都被儲存/載入。

一個非常簡單的案例

這些運算子在 serialize 函式內部用於儲存和載入類別資料成員。

這個函式庫包含一個名為 demo.cpp 的程式,它說明了如何使用這個系統。以下我們從這個程式中擷取程式碼,以最簡單的案例說明這個函式庫的預期用法。


#include <fstream>

// include headers that implement a archive in simple text format
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>

/////////////////////////////////////////////////////////////
// gps coordinate
//
// illustrates serialization for a simple type
//
class gps_position
{
private:
    friend class boost::serialization::access;
    // When the class Archive corresponds to an output archive, the
    // & operator is defined similar to <<.  Likewise, when the class Archive
    // is a type of input archive the & operator is defined similar to >>.
    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)
    {}
};

int main() {
    // create and open a character archive for output
    std::ofstream ofs("filename");

    // create class instance
    const gps_position g(35, 59, 24.567f);

    // save data to archive
    {
        boost::archive::text_oarchive oa(ofs);
        // write class instance to archive
        oa << g;
    	// archive and stream closed when destructors are called
    }

    // ... some time later restore the class instance to its orginal state
    gps_position newg;
    {
        // create and open an archive for input
        std::ifstream ifs("filename");
        boost::archive::text_iarchive ia(ifs);
        // read class state from archive
        ia >> newg;
        // archive and stream closed when destructors are called
    }
    return 0;
}

對於每個要透過序列化儲存的類別,必須存在一個函式來儲存所有定義類別狀態的類別成員。對於每個要透過序列化載入的類別,必須存在一個函式以與儲存時相同的順序載入這些類別成員。在上面的範例中,這些函式是由模板成員函式 serialize 產生的。

非侵入式版本

上述的公式是侵入式的。也就是說,它要求更改要序列化其執行個體的類別。在某些情況下,這可能會造成不便。系統允許的等效替代公式是


#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>

class gps_position
{
public:
    int degrees;
    int minutes;
    float seconds;
    gps_position(){};
    gps_position(int d, int m, float s) :
        degrees(d), minutes(m), seconds(s)
    {}
};

namespace boost {
namespace serialization {

template<class Archive>
void serialize(Archive & ar, gps_position & g, const unsigned int version)
{
    ar & g.degrees;
    ar & g.minutes;
    ar & g.seconds;
}

} // namespace serialization
} // namespace boost

在這種情況下,產生的 serialize 函式不是 gps_position 類別的成員。這兩種公式的功能完全相同。

非侵入式序列化的主要應用是允許在不更改類別定義的情況下實現序列化。為了實現這一點,類別必須公開足夠的資訊來重建類別狀態。在這個範例中,我們假設類別具有 public 成員 - 這並非常見的情況。只有公開足夠資訊來儲存和恢復類別狀態的類別才能在不更改類別定義的情況下進行序列化。

可序列化的成員

具有可序列化成員的可序列化類別看起來像這樣


class bus_stop
{
    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & latitude;
        ar & longitude;
    }
    gps_position latitude;
    gps_position longitude;
protected:
    bus_stop(const gps_position & lat_, const gps_position & long_) :
    latitude(lat_), longitude(long_)
    {}
public:
    bus_stop(){}
    // See item # 14 in Effective C++ by Scott Meyers.
    // re non-virtual destructors in base classes.
    virtual ~bus_stop(){}
};

也就是說,類別類型的成員與基本類型的成員一樣被序列化。

請注意,使用其中一個存檔運算子儲存類別 bus_stop 的執行個體將會呼叫 serialize 函式,該函式會儲存 latitudelongitude。這些成員又會透過在 gps_position 的定義中呼叫 serialize 來儲存。透過這種方式,整個資料結構只需將存檔運算子應用於其根項目即可儲存。

衍生類別

衍生類別應包含其基底類別的序列化。


#include <boost/serialization/base_object.hpp>

class bus_stop_corner : public bus_stop
{
    friend class boost::serialization::access;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        // serialize base class information
        ar & boost::serialization::base_object<bus_stop>(*this);
        ar & street1;
        ar & street2;
    }
    std::string street1;
    std::string street2;
    virtual std::string description() const
    {
        return street1 + " and " + street2;
    }
public:
    bus_stop_corner(){}
    bus_stop_corner(const gps_position & lat_, const gps_position & long_,
        const std::string & s1_, const std::string & s2_
    ) :
        bus_stop(lat_, long_), street1(s1_), street2(s2_)
    {}
};

請注意從衍生類別序列化基底類別。不要直接呼叫基底類別的 serialize 函式。這樣做可能看起來有效,但會繞過追蹤寫入儲存體的執行個體以消除冗餘的程式碼。它也會繞過將類別版本資訊寫入存檔的步驟。因此,建議始終將成員 serialize 函式設為私有。宣告 friend boost::serialization::access 將授予序列化函式庫存取私有成員變數和函式的權限。

指標

假設我們將公車路線定義為一個公車站牌的陣列。已知
  1. 我們可能有幾種類型的公車站牌(記住 bus_stop 是一個基底類別)
  2. 一個給定的 bus_stop 可能會出現在多條路線中。
使用指向 bus_stop 的指標陣列來表示公車路線很方便。

class bus_route
{
    friend class boost::serialization::access;
    bus_stop * stops[10];
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        int i;
        for(i = 0; i < 10; ++i)
            ar & stops[i];
    }
public:
    bus_route(){}
};

陣列 stops 的每個成員都會被序列化。但請記住,每個成員都是一個指標——那麼這究竟意味著什麼?序列化整個物件的目的是允許在另一個時間和地點重建原始資料結構。為了用指標完成這一點,僅儲存指標的值是不夠的,還必須儲存它指向的物件。稍後載入成員時,必須建立一個新物件,並且必須將一個新指標載入到類別成員中。

如果同一個指標被序列化多次,則只有一個實例會被添加到檔案中。當讀回時,不會讀回任何資料。唯一會發生的操作是將第二個指標設定為等於第一個指標。

請注意,在此範例中,陣列由多型指標組成。也就是說,每個陣列元素都指向幾種可能的公車站牌類型之一。因此,當指標被儲存時,必須儲存某種類別識別碼。載入指標時,必須讀取類別識別碼,並且必須建構相應類別的實例。最後,可以將資料載入到正確類型的新建立實例中。如 demo.cpp 中所示,透過基底類別指標序列化指向衍生類別的指標可能需要明確列舉要序列化的衍生類別。這稱為衍生類別的「註冊」或「匯出」。此需求及其滿足方法在 這裡 有詳細說明。

所有這些都由序列化程式庫自動完成。上述程式碼是完成透過指標存取的物件的儲存和載入所需的全部程式碼。

陣列

上述公式實際上比必要的更複雜。序列化程式庫會偵測到正在序列化的物件是否為陣列,並發出與上述程式碼等效的程式碼。因此,上述程式碼可以縮短為

class bus_route
{
    friend class boost::serialization::access;
    bus_stop * stops[10];
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & stops;
    }
public:
    bus_route(){}
};

STL 容器

上述範例使用成員陣列。更有可能的是,此類應用程式會使用 STL 集合來實現此目的。序列化程式庫包含用於序列化所有 STL 類別的程式碼。因此,以下的重新表述也能按預期工作。

#include <boost/serialization/list.hpp>

class bus_route
{
    friend class boost::serialization::access;
    std::list<bus_stop *> stops;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & stops;
    }
public:
    bus_route(){}
};

類別版本控制

假設我們對 bus_route 類別感到滿意,建構一個使用它的程式並交付產品。一段時間後,決定需要增強程式,並且 bus_route 類別被修改為包含路線駕駛員的姓名。所以新版本看起來像


#include <boost/serialization/list.hpp>
#include <boost/serialization/string.hpp>

class bus_route
{
    friend class boost::serialization::access;
    std::list<bus_stop *> stops;
    std::string driver_name;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        ar & driver_name;
        ar & stops;
    }
public:
    bus_route(){}
};

太好了,我們都完成了。除了……那些使用我們的應用程式的人現在有一堆在先前程式下建立的檔案怎麼辦?如何在新程式版本中使用這些檔案?

一般來說,序列化程式庫會在檔案中儲存每個序列化類別的版本號。預設情況下,此版本號為 0。載入檔案時,會讀取儲存檔案時的版本號。可以修改上述程式碼來利用這一點


#include <boost/serialization/list.hpp>
#include <boost/serialization/string.hpp>
#include <boost/serialization/version.hpp>

class bus_route
{
    friend class boost::serialization::access;
    std::list<bus_stop *> stops;
    std::string driver_name;
    template<class Archive>
    void serialize(Archive & ar, const unsigned int version)
    {
        // only save/load driver_name for newer archives
        if(version > 0)
            ar & driver_name;
        ar & stops;
    }
public:
    bus_route(){}
};

BOOST_CLASS_VERSION(bus_route, 1)

藉由對每個類別應用版本控制,無需嘗試維護檔案的版本控制。也就是說,檔案版本是其所有組成類別版本的組合。此系統允許程式始終與由先前所有程式版本建立的存檔相容,而無需付出比本範例更多的努力。

serialize 拆分為 save/load

serialize 函式簡單、簡潔,並保證類別成員以相同的順序儲存和載入 - 這是序列化系統的關鍵。然而,在某些情況下,載入和儲存操作不像這裡使用的範例那麼相似。例如,這可能發生在已經過多個版本演進的類別中。上述類別可以重新表述為

#include <boost/serialization/list.hpp>
#include <boost/serialization/string.hpp>
#include <boost/serialization/version.hpp>
#include <boost/serialization/split_member.hpp>

class bus_route
{
    friend class boost::serialization::access;
    std::list<bus_stop *> stops;
    std::string driver_name;
    template<class Archive>
    void save(Archive & ar, const unsigned int version) const
    {
        // note, version is always the latest when saving
        ar  & driver_name;
        ar  & stops;
    }
    template<class Archive>
    void load(Archive & ar, const unsigned int version)
    {
        if(version > 0)
            ar & driver_name;
        ar  & stops;
    }
    BOOST_SERIALIZATION_SPLIT_MEMBER()
public:
    bus_route(){}
};

BOOST_CLASS_VERSION(bus_route, 1)

巨集 BOOST_SERIALIZATION_SPLIT_MEMBER() 產生的程式碼會根據存檔是用於儲存還是載入來呼叫 saveload

存檔

我們這裡的討論集中在為類別添加序列化功能。要序列化的數據的實際呈現是在存檔類別中實現的。因此,序列化數據流是類別序列化和所選存檔的產物。一個關鍵的設計決策是這兩個組件要獨立。這允許任何序列化規範可與任何存檔一起使用。

在本教學中,我們使用了一個特定的存檔類別 - text_oarchive 用於儲存,text_iarchive 用於載入。文字存檔將數據呈現為文字,並且可在不同平台之間移植。除了文字存檔之外,該程式庫還包含用於原生二進制數據和 xml 格式數據的存檔類別。所有存檔類別的介面都相同。一旦為類別定義了序列化,該類別就可以序列化到任何類型的存檔。

如果目前的存檔類別集沒有提供特定應用程式所需的屬性、格式或行為,則可以建立新的存檔類別或從現有存檔類別衍生。這將在手冊的後續部分中進行說明。

範例列表

demo.cpp
這是本教學中使用的完整範例。它執行以下操作
  1. 建立一個包含不同種類的停靠點、路線和時間表的結構
  2. 顯示它
  3. 用一條語句將其序列化到名為「testfile.txt」的檔案中
  4. 還原到另一個結構
  5. 顯示還原的結構
此程式的輸出足以驗證此系統是否滿足所有最初提出的序列化系統需求。由於序列化檔案是 ASCII 文字,因此也可以顯示 存檔檔案的內容
demo_xml.cpp
這是原始 demo 的變體,除了其他存檔外,它還支援 xml 存檔。需要額外的包裝巨集 BOOST_SERIALIZATION_NVP(name) 將數據項名稱與相應的 xml 標籤關聯。'name' 必須是有效的 xml 標籤,否則將無法還原存檔。更多資訊請參見 名稱-值對這裡 顯示了 xml 存檔的樣子。
demo_xml_save.cppdemo_xml_load.cpp
還需注意的是,雖然我們的範例將程式資料儲存並載入到同一個程式內的存檔,但这僅僅是為了方便說明。一般來說,建立存檔的程式不一定會載入該存檔。

敏銳的讀者可能會注意到這些範例中存在一個細微但重要的缺陷:它們會造成記憶體洩漏。公車站牌是在 main 函式中建立的。公車時刻表可能會多次參考這些公車站牌。在 `main` 函式結束,公車時刻表被銷毀後,公車站牌也會被銷毀。這看起來沒問題。但是,從存檔載入過程中建立的 new_schedule 資料項目中的結構呢?它包含自己獨立的一組公車站牌,這些站牌在公車時刻表之外沒有被參考。這些站牌在程式中任何地方都不會被銷毀——這就是記憶體洩漏。

有幾種方法可以解決這個問題。一種方法是明確地管理公車站牌。然而,更穩健且更透明的方法是使用 shared_ptr 而不是原始指標。除了標準函式庫的序列化實現之外,序列化函式庫還包含了 boost::shared_ptr 的序列化實現。有了這個,修改任何這些範例來消除記憶體洩漏應該很容易。這就留給讀者作為練習。


© Copyright Robert Ramey 2002-2004。依據 Boost 軟體授權條款 1.0 版散布。(請參閱隨附檔案 LICENSE_1_0.txt 或至 https://boost.dev.org.tw/LICENSE_1_0.txt 複製)