|
序列化教學 |
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
函式,該函式會儲存 latitude
和 longitude
。這些成員又會透過在 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
將授予序列化函式庫存取私有成員變數和函式的權限。
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(){}
};
#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()
產生的程式碼會根據存檔是用於儲存還是載入來呼叫 save
或 load
。在本教學中,我們使用了一個特定的存檔類別 - text_oarchive
用於儲存,text_iarchive
用於載入。文字存檔將數據呈現為文字,並且可在不同平台之間移植。除了文字存檔之外,該程式庫還包含用於原生二進制數據和 xml 格式數據的存檔類別。所有存檔類別的介面都相同。一旦為類別定義了序列化,該類別就可以序列化到任何類型的存檔。
如果目前的存檔類別集沒有提供特定應用程式所需的屬性、格式或行為,則可以建立新的存檔類別或從現有存檔類別衍生。這將在手冊的後續部分中進行說明。
敏銳的讀者可能會注意到這些範例中存在一個細微但重要的缺陷:它們會造成記憶體洩漏。公車站牌是在 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 複製)