C++序列化對象

需求

.  在寫代碼的過程當中,常常會須要把代碼層面的對象數據保存到文件,而這些數據會以各類格式存儲.例如:json,xml,二進制等等.最近剛好就須要把對象以二進制存儲到硬盤.這是一個很簡單的需求,相比json,xml格式,二進制是直接把字節copy到硬盤,沒有中間商賺差價,因此這實現起來相對容易.c++

實現

struct Vec3 {
    float x;
    float y;
    float z;
}

.  上面是一個簡單的三維向量結構體,如何把它序列化到文件呢?json

Vec3 v;
v.x = 1.0f;
v.y = 2.0f;
v.z = 3.0f;
os.write((const char *)&v, sizeof(Vec3));

.  上述是序列化Vec3對象數據到文件的代碼,很是直接.它的內存佈局是3個浮點型變量緊湊排列,要把它存儲到硬盤,只要從頭至尾按字節拷貝便可.可是,在實際開發中,要序列化的對象不可能所有都是內存緊湊排列的,例如STL容器.數組

std::vector<Vec3> vec;

.  若是將容器變量從頭至尾拷貝到文件,必然會出現錯誤.由於容器內部經過一個指針來訪問存儲的對象,而直接拷貝這個容器,只會把指針拷貝,指針指向的數據卻丟失了.可是,容器提供了一個能夠直接訪問指針指向數據的接口,咱們能夠經過這個接口獲得數據而後直接拷貝.安全

os.write((const char *)&vec, vec.size() * sizeof(Vec3));        //  錯誤, 僅拷貝指針
os.write((const char *)vec.data(), vec.size() * sizeof(Vec3));  //  正確, 數據被徹底拷貝

.  經過這個方法就能夠獲得正確的拷貝結果了.一般,好的作法是將序列化和反序列化封裝成接口,以便於使用,如何封裝接口,就是這篇文章的主題.佈局

.  從上述兩個例子能夠發現,對於單體對象和數組對象,編寫的代碼是不同的,單體對象直接拷貝,數組對象須要經過 .data() 取得數據地址再進行拷貝.而考慮到還有嵌套數組對象 std::vector<std::vector<Vec3>>.對於嵌套數組序列化的代碼可能以下:指針

std::vector<std::vector<Vec3>> vec2;
for (auto & vec: vec2)
{
    os.write((const char *)vec.data(), vec.size() * sizeof(Vec3));
}

.  能夠發現,對嵌套數組對象的序列化代碼跟上述2種對象又不同,考慮到還有N層嵌套的數組對象.此外,在C++中有一個可平凡複製的概念,通俗的說,就是能夠直接按字節拷貝的結構稱之爲可平凡複製,上述的Vec3則是一個可平凡複製結構,而STL容器則不是可平凡複製結構,除此以外還有更多不可平凡複製且非容器的結構,故此,若是要封裝接口,除了區分單體對象和數組對象,還要區分可平凡複製和不可平凡複製.code

void Serialize(std::ostream & os,   const Type & val);  //  序列化
void Deserialize(std::istream & is,       Type & val);  //  反序列化

.  上面是比較理想的接口原型,序列化/反序列化各一個接口,腦補一下,這兩個接口的實現應該是怎樣的?最直接的實現是對每一種類型重載一個定義,例如:xml

//  string
void Serialize(std::ostream & os, const std::string & val)
{
    os.write(str.data(), str.size());
}
//  vector<int>
void Serialize(std::ostream & os, const std::vector<int> & val)
{
    os.write(str.data(), str.size() * sizeof(int));
}
//  vector<string>
void Serialize(std::ostream & os, const std::vector<std::string> & val)
{
    for (auto & str: val)
    {
        Serialize(os, str);
    }
}

//  接口調用
std::string str;
std::vector<int> vecint;
std::vector<std::string> vecstr;
Serialize(os, str);
Serialize(os, vecint);
Serialize(os, vecstr);

.  從上面能夠看出,接口統一,使用方便.可是對每一種類型都重載,要寫的代碼實在太多了,萬一要序列化一個多層嵌套數組,會寫的懷疑人生.藉助C++強大的語言特性,這一切均可以一步到位.對象

//  可平凡複製
template <class T, typename std::enable_if_t<std::is_trivially_copyable_v<T>, int> N = 0>
void Serialize(std::ostream & os, const T & val)
{
    os.write((const char *)&val, sizeof(T));
}

//  容器
template <class T, typename std::enable_if_t<
    std::is_same_v<typename T::iterator, decltype(std::declval<T>().begin())> &&
    std::is_same_v<typename T::iterator, decltype(std::declval<T>().end())> &&
    std::is_trivially_copyable_v<typename T::value_type>, int> N = 0>
    void Serialize(std::ostream & os, const T & val)
{
    unsigned int size = val.size();
    os.write((const char *)&size, sizeof(size));
    os.write((const char *)val.data(), size * sizeof(typename T::value_type));
}

template <class T, typename std::enable_if_t<
    std::is_same_v<typename T::iterator, decltype(std::declval<T>().begin())> &&
    std::is_same_v<typename T::iterator, decltype(std::declval<T>().end())> &&
    !std::is_trivially_copyable_v<typename T::value_type>, int> N = 0>
void Serialize(std::ostream & os, const T & val)
{
    unsigned int size = val.size();
    os.write((const char *)&size, sizeof(size));
    for (auto & v : val) { Serialize(os, v); }
}

//  可平凡複製
template <class T, typename std::enable_if_t<std::is_trivially_copyable_v<T>, int> N = 0>
void Deserialize(std::istream & is, T & val)
{
    is.read((char *)&val, sizeof(T));
}

//  容器
template <class T, typename std::enable_if_t<
    std::is_same_v<typename T::iterator, decltype(std::declval<T>().begin())> &&
    std::is_same_v<typename T::iterator, decltype(std::declval<T>().end())> &&
    std::is_trivially_copyable_v<typename T::value_type>, int> N = 0>
    void Deserialize(std::istream & is, T & val)
{
    unsigned int size = 0;
    is.read((char *)&size, sizeof(unsigned int));
    val.resize(size);
    is.read((char *)val.data(), size * sizeof(typename T::value_type));
}

template <class T, typename std::enable_if_t<
    std::is_same_v<typename T::iterator, decltype(std::declval<T>().begin())> &&
    std::is_same_v<typename T::iterator, decltype(std::declval<T>().end())> &&
    !std::is_trivially_copyable_v<typename T::value_type>, int> N = 0>
void Deserialize(std::istream & is, T & val)
{
    unsigned int size = 0;
    is.read((char *)&size, sizeof(unsigned int));
    val.resize(size);
    for (auto & v : val) { Deserialize(is, v); }
}

.  以上實現可序列化任意可平凡拷貝結構,而且也可序列化任意嵌套層數的STL風格數組.而對於不可平凡複製結構,只須要針對該結構重載便可.藉助C++強大的類型推導機制和SFINEA機制,可保證類型安全又具有可擴展性.接口

相關文章
相關標籤/搜索