基于泛型編程的序列化實(shí)現(xiàn)方法

寫在前面
序列化是一個(gè)轉(zhuǎn)儲(chǔ)-恢復(fù)的操作過(guò)程,即支持將一個(gè)對(duì)象轉(zhuǎn)儲(chǔ)到臨時(shí)緩沖或者永久文件中和恢復(fù)臨時(shí)緩沖或者永久文件中的內(nèi)容到一個(gè)對(duì)象中等操作,其目的是可以在不同的應(yīng)用程序之間共享和傳輸數(shù)據(jù),以達(dá)到跨應(yīng)用程序、跨語(yǔ)言和跨平臺(tái)的解耦,以及當(dāng)應(yīng)用程序在客戶現(xiàn)場(chǎng)發(fā)生異?;蛘弑罎r(shí)可以即時(shí)保存數(shù)據(jù)結(jié)構(gòu)各內(nèi)容的值到文件中,并在發(fā)回給開(kāi)發(fā)者時(shí)再恢復(fù)數(shù)據(jù)結(jié)構(gòu)各內(nèi)容的值以協(xié)助分析和定位原因。
泛型編程是一個(gè)對(duì)具有相同功能的不同類型的抽象實(shí)現(xiàn)過(guò)程,比如STL的源碼實(shí)現(xiàn),其支持在編譯期由編譯器自動(dòng)推導(dǎo)具體類型并生成實(shí)現(xiàn)代碼,同時(shí)依據(jù)具體類型的特定性質(zhì)或者優(yōu)化需要支持使用特化或者偏特化及模板元編程等特性進(jìn)行具體實(shí)現(xiàn)。
Hello World

成都創(chuàng)新互聯(lián)公司專業(yè)為企業(yè)提供西峰網(wǎng)站建設(shè)、西峰做網(wǎng)站、西峰網(wǎng)站設(shè)計(jì)、西峰網(wǎng)站制作等企業(yè)網(wǎng)站建設(shè)、網(wǎng)頁(yè)設(shè)計(jì)與制作、西峰企業(yè)網(wǎng)站模板建站服務(wù),十年西峰做網(wǎng)站經(jīng)驗(yàn),不只是建網(wǎng)站,更提供有價(jià)值的思路和整體網(wǎng)絡(luò)服務(wù)。

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

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

namespace std
{
    typedef basic_ostream<char>         ostream;
}

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

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

如使用下面的方式則會(huì)出現(xiàn)一連串的編譯錯(cuò)誤

MyClip clip;
std::cout << clip;

錯(cuò)誤內(nèi)容大致都是一些clip不支持<<操作符并在嘗試將clip轉(zhuǎn)為cout支持的一系列的內(nèi)建類型如void*和int等等類型時(shí)轉(zhuǎn)換操作不支持等信息。

為了解決編譯錯(cuò)誤,我們則需要將類MyClip支持輸入輸出操作符>>和<<,類似實(shí)現(xiàn)代碼如下

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;
}

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

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

這種序列化的實(shí)現(xiàn)方法是非常直觀而且容易理解的,但缺陷是對(duì)于大型的項(xiàng)目開(kāi)發(fā)中,由于自定義類型的數(shù)量較多,可能達(dá)到成千上萬(wàn)個(gè)甚至更多時(shí),對(duì)于每個(gè)類型我們則需要實(shí)現(xiàn)2個(gè)函數(shù),一個(gè)是序列化轉(zhuǎn)儲(chǔ)數(shù)據(jù),另一個(gè)則是反序列化恢復(fù)數(shù)據(jù),不僅僅增加了開(kāi)發(fā)實(shí)現(xiàn)的代碼數(shù)量,如果后期一旦對(duì)部分類的成員變量有所修改,則需要同時(shí)修改這2個(gè)函數(shù)。
同時(shí)考慮到更復(fù)雜的自定義類型,比如含有繼承關(guān)系和自定義類型的成員變量

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

上述代碼需要轉(zhuǎn)儲(chǔ)-恢復(fù)類MyVideo的對(duì)象內(nèi)容時(shí),事情會(huì)變得更復(fù)雜些,因?yàn)檫€需要轉(zhuǎn)儲(chǔ)-恢復(fù)基類,同時(shí)成員變量使用了STL模板容器list與自定義類'MyFilter`的結(jié)合,這種情況也需要自己去定義轉(zhuǎn)儲(chǔ)-恢復(fù)的實(shí)現(xiàn)方式。
針對(duì)以上疑問(wèn),有沒(méi)有一種方法能減少我們代碼修改的工作量,同時(shí)又易于理解和維護(hù)呢?
Boost序列化庫(kù)
對(duì)于使用C++標(biāo)準(zhǔn)輸入輸出的方法遇到的問(wèn)題,好在Boost提供了一種良好的解決方式,則是將所有類型的轉(zhuǎn)儲(chǔ)-恢復(fù)操作抽象到一個(gè)函數(shù)中,易于理解,如對(duì)于上述類型,只需要將上述的2個(gè)友元函數(shù)替換為下面的一個(gè)友元函數(shù)

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

友元函數(shù)的實(shí)現(xiàn)類似下面的樣子

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內(nèi)部定義的一個(gè)宏,其主要作用是對(duì)各個(gè)變量進(jìn)行打包。

轉(zhuǎn)儲(chǔ)-恢復(fù)的使用則直接作用于操作符>>和<<,比如

// 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即是分別從字符串流中恢復(fù)數(shù)據(jù)以及將類對(duì)象的數(shù)據(jù)轉(zhuǎn)儲(chǔ)到字符串流中。
對(duì)于類MyFilter和MyVideo則使用相同的方式,即分別增加一個(gè)友元模板函數(shù)serialize的實(shí)現(xiàn)即可,至于std::list模板類,boost已經(jīng)幫我們實(shí)現(xiàn)了。
這時(shí)我們發(fā)現(xiàn),對(duì)于每一個(gè)定義的類,我們需要做的僅僅是在類內(nèi)部聲明一個(gè)友元模板函數(shù),同時(shí)類外部實(shí)現(xiàn)這個(gè)模板函數(shù)即可,對(duì)于后期類的成員變量的修改,如增加、刪除或者重命名成員變量,也僅僅是修改一個(gè)函數(shù)即可。
Boost序列化庫(kù)已經(jīng)足夠完美了,但故事并未結(jié)束!
在用于端上開(kāi)發(fā)時(shí),我們發(fā)現(xiàn)引用Boost序列化庫(kù)遇到了幾個(gè)挑戰(zhàn)

端上的編譯資料很少,官方對(duì)端上編譯的資料基本沒(méi)有,在切換不同的版本進(jìn)行編譯時(shí)經(jīng)常會(huì)遇到各種奇怪的編譯錯(cuò)誤問(wèn)題
Boost在不同的C++開(kāi)發(fā)標(biāo)準(zhǔn)之間兼容性不夠好,尤其是使用libc++標(biāo)準(zhǔn)進(jìn)行編譯鏈接時(shí)遇到的問(wèn)題較多
Boost增加了端上發(fā)行包的體積
Boost每次序列化都會(huì)增加序列化庫(kù)及版本號(hào)等私有頭信息,反序列化時(shí)再重新解析,降低了部分場(chǎng)景下的使用性能

基于泛型編程的序列化實(shí)現(xiàn)方法
為了解決使用Boost遇到的這些問(wèn)題,我們覺(jué)得有必要重新實(shí)現(xiàn)序列化庫(kù),以剝離對(duì)Boost的依賴,同時(shí)能滿足如下要求

由于現(xiàn)有工程大量使用了Boost序列化庫(kù),因此兼容現(xiàn)有的代碼以及開(kāi)發(fā)者的習(xí)慣是首要目標(biāo)
盡量使得代碼修改和重構(gòu)的工作量最小
兼容不同的C++開(kāi)發(fā)標(biāo)準(zhǔn)
提供比Boost序列化庫(kù)更高的性能
降低端上發(fā)行包的體積

為了兼容現(xiàn)有使用Boost的代碼以及保持當(dāng)前開(kāi)發(fā)者的習(xí)慣,同時(shí)使用代碼修改的重構(gòu)的工作量最小,我們應(yīng)該保留模板函數(shù)serialize,同時(shí)對(duì)于模板函數(shù)內(nèi)部的實(shí)現(xiàn),為了提高效率也不需要對(duì)各成員變量重新打包,即直接使用如下定義

#define BOOST_SERIALIZATION_NVP(value)  value

對(duì)于轉(zhuǎn)儲(chǔ)-恢復(fù)的接口調(diào)用,仍然延續(xù)目前的調(diào)用方式,只是將輸入輸出類修改為

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

好了,到此為止,序列化庫(kù)對(duì)外的接口工作已經(jīng)做好,剩下的就是內(nèi)部的事情,應(yīng)該如何重新設(shè)計(jì)和實(shí)現(xiàn)序列化庫(kù)的內(nèi)部框架才能滿足要求呢?

先來(lái)看一下當(dāng)前的設(shè)計(jì)架構(gòu)的處理流程圖
基于泛型編程的序列化實(shí)現(xiàn)方法
比如對(duì)于轉(zhuǎn)儲(chǔ)類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);

開(kāi)發(fā)者調(diào)用操作符函數(shù)<<時(shí),需要首先回調(diào)到相應(yīng)類型的模板函數(shù)serialize中

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

當(dāng)開(kāi)始對(duì)具體類型的各個(gè)成員進(jìn)行操作時(shí),這時(shí)需要進(jìn)行判斷,如果此成員變量的類型已經(jīng)是內(nèi)建類型,則直接進(jìn)行序列化,如果是自定義類型,則需要重新回調(diào)到對(duì)應(yīng)類型的模板函數(shù)serialize中

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

上述代碼中的basic_save::invoke則會(huì)在編譯期完成模板類型推導(dǎo)并選擇直接對(duì)內(nèi)建類型進(jìn)行轉(zhuǎn)儲(chǔ)還是重新回調(diào)到成員變量對(duì)應(yīng)類型的serialize函數(shù)繼續(xù)重復(fù)上述過(guò)程。
由于內(nèi)建類型數(shù)量有限,因此這里我們選擇使模板類basic_save的默認(rèn)行為為回調(diào)到相應(yīng)類型的serialize函數(shù)中

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>
{
};

這時(shí)會(huì)發(fā)現(xiàn)上述代碼的模板參數(shù)多了一個(gè)參數(shù)E,這里主要是需要對(duì)枚舉類型進(jìn)行特殊處理,使用偏特化的實(shí)現(xiàn)如下

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;
    }
};

到這里我們已經(jīng)完成了重載操作符&的默認(rèn)行為,即是不斷進(jìn)行回溯到相應(yīng)的成員變量的類型中的模板函數(shù)serialize中,但對(duì)于碰到內(nèi)建模型時(shí),我們則需要讓這個(gè)回溯過(guò)程停止,比如對(duì)于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>
{
};

這里對(duì)于int類型,則直接轉(zhuǎn)儲(chǔ)整數(shù)值到輸出流中,此時(shí)text_oarchive則還需要增加一個(gè)終極轉(zhuǎn)儲(chǔ)函數(shù)

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

這里我們發(fā)現(xiàn),在save成員函數(shù)中,我們已經(jīng)將具體的成員變量的值輸出到流中了。

對(duì)于其它的內(nèi)建類型,則使用相同的方式處理,要以參考C++ std::basic_ostream的源碼實(shí)現(xiàn)。

相應(yīng)的,對(duì)于恢復(fù)操作的text_iarchive的操作流程如下圖
基于泛型編程的序列化實(shí)現(xiàn)方法

測(cè)試結(jié)果
我們對(duì)使用Boost以及重新實(shí)現(xiàn)的序列化庫(kù)進(jìn)行了對(duì)比測(cè)試,其結(jié)果如下

代碼修改的重構(gòu)的工作非常小,只需要?jiǎng)h除Boost的相關(guān)頭文件,以及將boost相關(guān)命名空間替換為alivc,BOOST_SERIALIZATION_FUNCTION以及BOOST_SERIALIZATION_NVP的宏替換
Android端下的發(fā)行包體積大概減少了500KB
目前的消息處理框架中,處理一次消息的平均時(shí)間由100us降低到了25us
代碼實(shí)現(xiàn)約300行,更輕量級(jí)

未來(lái)還能做什么
由于當(dāng)前項(xiàng)目的原因,重新實(shí)現(xiàn)的序列化還沒(méi)有支持轉(zhuǎn)儲(chǔ)-恢復(fù)指針?biāo)赶虻膬?nèi)存數(shù)據(jù),但當(dāng)前的設(shè)計(jì)框架已經(jīng)考慮了這種拓展性,未來(lái)會(huì)考慮支持。
總結(jié)

泛型編程能夠大幅提高開(kāi)發(fā)效率,尤其是在代碼重用方面能發(fā)揮其優(yōu)勢(shì),同時(shí)由于其類型推導(dǎo)及生成代碼均在編譯期完成,并不會(huì)降低性能
序列化對(duì)于需要進(jìn)行轉(zhuǎn)儲(chǔ)-恢復(fù)的解耦處理以及協(xié)助定位異常和崩潰的原因分析具有重要作用
利用C++及模板自身的語(yǔ)言特性優(yōu)勢(shì),結(jié)合合理的架構(gòu)設(shè)計(jì),即易于拓展又能盡量避免過(guò)度設(shè)計(jì)

覺(jué)得不錯(cuò)請(qǐng)點(diǎn)贊支持,歡迎留言或進(jìn)我的個(gè)人群855801563領(lǐng)取【架構(gòu)資料專題目合集90期】、【BATJTMD大廠JAVA面試真題1000+】,本群專用于學(xué)習(xí)交流技術(shù)、分享面試機(jī)會(huì),拒絕廣告,我也會(huì)在群內(nèi)不定期答題、探討。

分享名稱:基于泛型編程的序列化實(shí)現(xiàn)方法
URL地址:http://bm7419.com/article46/gocceg.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供外貿(mào)網(wǎng)站建設(shè)Google、域名注冊(cè)、外貿(mào)建站、網(wǎng)站維護(hù)、網(wǎng)站設(shè)計(jì)公司

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

成都seo排名網(wǎng)站優(yōu)化