基於泛型編程的序列化實現方法

寫在前面
序列化是一個轉儲-恢復的操作過程,即支持將一個對象轉儲到臨時緩衝或者永久文件中和恢復臨時緩衝或者永久文件中的內容到一個對象中等操作,其目的是可以在不同的應用程序之間共享和傳輸數據,以達到跨應用程序、跨語言和跨平臺的解耦,以及當應用程序在客戶現場發生異常或者崩潰時可以即時保存數據結構各內容的值到文件中,並在發回給開發者時再恢復數據結構各內容的值以協助分析和定位原因。
泛型編程是一個對具有相同功能的不同類型的抽象實現過程,比如STL的源碼實現,其支持在編譯期由編譯器自動推導具體類型並生成實現代碼,同時依據具體類型的特定性質或者優化需要支持使用特化或者偏特化及模板元編程等特性進行具體實現。
Hello World

#include <iostream>
int main(int argc, char* argv[])
{
    std::cout << "Hello World!" << std::endl;
    return 0;
}

泛型編程其實就在我們身邊,我們經常使用的std和stl命名空間中的函數和類很多都是泛型編程實現的,如上述代碼中的std::cout即是模板類std::basic_ostream的一種特化

namespace std
{
    typedef basic_ostream<char>         ostream;
}

從C++的標準輸入輸出開始
除了上述提到的std::cout和std::basic_ostream外,C++還提供了各種形式的輸入輸出模板類,如std::basic_istream,std::basic_ifstream,std::basic_ofstream, std::basic_istringstream,std::basic_ostringstream等等,其主要實現了內建類型(built-in)的輸入輸出接口,比如對於Hello World可直接使用於字符串,然而對於自定義類型的輸入輸出,則需要重載實現操作符>>和<<,如對於下面的自定義類

class MyClip
{
    bool        mValid;
    int         mIn;
    int         mOut;
    std::string mFilePath;
};

如使用下面的方式則會出現一連串的編譯錯誤

MyClip clip;
std::cout << clip;

錯誤內容大致都是一些clip不支持<<操作符並在嘗試將clip轉爲cout支持的一系列的內建類型如void*和int等等類型時轉換操作不支持等信息。

爲了解決編譯錯誤,我們則需要將類MyClip支持輸入輸出操作符>>和<<,類似實現代碼如下

inline std::istream& operator>>(std::istream& st, MyClip& clip)
{
    st >> clip.mValid;
    st >> clip.mIn >> clip.mOut;
    st >> clip.mFilePath;
    return st;
}
inline std::ostream& operator<<(std::ostream& st, MyClip const& clip)
{
    st << clip.mValid << ' ';
    st << clip.mIn << ' ' << clip.mOut << ' ';
    st << clip.mFilePath << ' ';
    return st;
}

爲了能正常訪問類對象的私有成員變量,我們還需要在自定義類型裏面增加序列化和反序列化的友元函數(回憶一下這裏爲何必須使用友元函數而不能直接重載操作符>>和<<?),如

friend std::istream& operator>>(std::istream& st, MyClip& clip);
friend std::ostream& operator<<(std::ostream& st, MyClip const& clip);

這種序列化的實現方法是非常直觀而且容易理解的,但缺陷是對於大型的項目開發中,由於自定義類型的數量較多,可能達到成千上萬個甚至更多時,對於每個類型我們則需要實現2個函數,一個是序列化轉儲數據,另一個則是反序列化恢復數據,不僅僅增加了開發實現的代碼數量,如果後期一旦對部分類的成員變量有所修改,則需要同時修改這2個函數。
同時考慮到更復雜的自定義類型,比如含有繼承關係和自定義類型的成員變量

class MyVideo : public MyClip
{
    std::list<MyFilter> mFilters;
};

上述代碼需要轉儲-恢復類MyVideo的對象內容時,事情會變得更復雜些,因爲還需要轉儲-恢復基類,同時成員變量使用了STL模板容器list與自定義類'MyFilter`的結合,這種情況也需要自己去定義轉儲-恢復的實現方式。
針對以上疑問,有沒有一種方法能減少我們代碼修改的工作量,同時又易於理解和維護呢?
Boost序列化庫
對於使用C++標準輸入輸出的方法遇到的問題,好在Boost提供了一種良好的解決方式,則是將所有類型的轉儲-恢復操作抽象到一個函數中,易於理解,如對於上述類型,只需要將上述的2個友元函數替換爲下面的一個友元函數

template<typename Archive> friend void serialize(Archive&, MyClip&, unsigned int const);

友元函數的實現類似下面的樣子

template<typename A>void serialize(A &ar, MyClip &clip, unsigned int const ver)
{
    ar & BOOST_SERIALIZATION_NVP(clip.mValid);
    ar & BOOST_SERIALIZATION_NVP(clip.mIn);
    ar & BOOST_SERIALIZATION_NVP(clip.mOut);
    ar & BOOST_SERIALIZATION_NVP(clip.mFilePath);
}

其中BOOST_SERIALIZATION_NVP是Boost內部定義的一個宏,其主要作用是對各個變量進行打包。

轉儲-恢復的使用則直接作用於操作符>>和<<,比如

// store
MyClip clip;
······
std::ostringstream ostr;
boost::archive::text_oarchive oa(ostr);
oa << clip;

// load
std::istringstream istr(ostr.str());
boost::archive::text_iarchive ia(istr);
ia >> clip;

這裏使用的std::istringstream和std::ostringstream即是分別從字符串流中恢復數據以及將類對象的數據轉儲到字符串流中。
對於類MyFilter和MyVideo則使用相同的方式,即分別增加一個友元模板函數serialize的實現即可,至於std::list模板類,boost已經幫我們實現了。
這時我們發現,對於每一個定義的類,我們需要做的僅僅是在類內部聲明一個友元模板函數,同時類外部實現這個模板函數即可,對於後期類的成員變量的修改,如增加、刪除或者重命名成員變量,也僅僅是修改一個函數即可。
Boost序列化庫已經足夠完美了,但故事並未結束!
在用於端上開發時,我們發現引用Boost序列化庫遇到了幾個挑戰

端上的編譯資料很少,官方對端上編譯的資料基本沒有,在切換不同的版本進行編譯時經常會遇到各種奇怪的編譯錯誤問題
Boost在不同的C++開發標準之間兼容性不夠好,尤其是使用libc++標準進行編譯鏈接時遇到的問題較多
Boost增加了端上發行包的體積
Boost每次序列化都會增加序列化庫及版本號等私有頭信息,反序列化時再重新解析,降低了部分場景下的使用性能

基於泛型編程的序列化實現方法
爲了解決使用Boost遇到的這些問題,我們覺得有必要重新實現序列化庫,以剝離對Boost的依賴,同時能滿足如下要求

由於現有工程大量使用了Boost序列化庫,因此兼容現有的代碼以及開發者的習慣是首要目標
儘量使得代碼修改和重構的工作量最小
兼容不同的C++開發標準
提供比Boost序列化庫更高的性能
降低端上發行包的體積

爲了兼容現有使用Boost的代碼以及保持當前開發者的習慣,同時使用代碼修改的重構的工作量最小,我們應該保留模板函數serialize,同時對於模板函數內部的實現,爲了提高效率也不需要對各成員變量重新打包,即直接使用如下定義

#define BOOST_SERIALIZATION_NVP(value)  value

對於轉儲-恢復的接口調用,仍然延續目前的調用方式,只是將輸入輸出類修改爲

alivc::text_oarchive oa(ostr);
alivc::text_iarchive ia(istr);

好了,到此爲止,序列化庫對外的接口工作已經做好,剩下的就是內部的事情,應該如何重新設計和實現序列化庫的內部框架才能滿足要求呢?

先來看一下當前的設計架構的處理流程圖
基於泛型編程的序列化實現方法
比如對於轉儲類text_oarchive,其支持的接口必須包括

explicit text_oarchive(std::ostream& ost, unsigned int version = 0);
template <typename T> text_oarchive& operator<<(T& v);
template <typename T> text_oarchive& operator&(T& v);

開發者調用操作符函數<<時,需要首先回調到相應類型的模板函數serialize中

template <typename T>
text_oarchive& operator<<(T& v)
{
    serialize(*this, v, mversion);
    return *this;
}

當開始對具體類型的各個成員進行操作時,這時需要進行判斷,如果此成員變量的類型已經是內建類型,則直接進行序列化,如果是自定義類型,則需要重新回調到對應類型的模板函數serialize中

template <typename T>
text_oarchive& operator&(T& v)
{
    basic_save<T>::invoke(*this, v, mversion);
    return *this;
}

上述代碼中的basic_save::invoke則會在編譯期完成模板類型推導並選擇直接對內建類型進行轉儲還是重新回調到成員變量對應類型的serialize函數繼續重複上述過程。
由於內建類型數量有限,因此這裏我們選擇使模板類basic_save的默認行爲爲回調到相應類型的serialize函數中

template <typename T, bool E = false>
struct basic_load_save
{
    template <typename A>
    static void invoke(A& ar, T& v, unsigned int version)
    {
        serialize(ar, v, version);
    }
};

template <typename T>
struct basic_save : public basic_load_save<T, std::is_enum<T>::value>
{
};

這時會發現上述代碼的模板參數多了一個參數E,這裏主要是需要對枚舉類型進行特殊處理,使用偏特化的實現如下

template <typename T>
struct basic_load_save<T, true>
{
    template <typename A>
    static void invoke(A& ar, T& v, unsigned int version)
    {
        int tmp = v;
        ar & tmp;
        v = (T)tmp;
    }
};

到這裏我們已經完成了重載操作符&的默認行爲,即是不斷進行回溯到相應的成員變量的類型中的模板函數serialize中,但對於碰到內建模型時,我們則需要讓這個回溯過程停止,比如對於int類型

template <typename T>
struct basic_pod_save
{
    template <typename A>
    static void invoke(A& ar, T const& v, unsigned int)
    {
        ar.template save(v);
    }
};

template <>
struct basic_save<int> : public basic_pod_save<int>
{
};

這裏對於int類型,則直接轉儲整數值到輸出流中,此時text_oarchive則還需要增加一個終極轉儲函數

template <typename T>
void save(T const& v)
{
    most << v << ' ';
}

這裏我們發現,在save成員函數中,我們已經將具體的成員變量的值輸出到流中了。

對於其它的內建類型,則使用相同的方式處理,要以參考C++ std::basic_ostream的源碼實現。

相應的,對於恢復操作的text_iarchive的操作流程如下圖
基於泛型編程的序列化實現方法

測試結果
我們對使用Boost以及重新實現的序列化庫進行了對比測試,其結果如下

代碼修改的重構的工作非常小,只需要刪除Boost的相關頭文件,以及將boost相關命名空間替換爲alivc,BOOST_SERIALIZATION_FUNCTION以及BOOST_SERIALIZATION_NVP的宏替換
Android端下的發行包體積大概減少了500KB
目前的消息處理框架中,處理一次消息的平均時間由100us降低到了25us
代碼實現約300行,更輕量級

未來還能做什麼
由於當前項目的原因,重新實現的序列化還沒有支持轉儲-恢復指針所指向的內存數據,但當前的設計框架已經考慮了這種拓展性,未來會考慮支持。
總結

泛型編程能夠大幅提高開發效率,尤其是在代碼重用方面能發揮其優勢,同時由於其類型推導及生成代碼均在編譯期完成,並不會降低性能
序列化對於需要進行轉儲-恢復的解耦處理以及協助定位異常和崩潰的原因分析具有重要作用
利用C++及模板自身的語言特性優勢,結合合理的架構設計,即易於拓展又能儘量避免過度設計

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章