天天看點

C++反射機制:可變參數模闆實作C++反射(二)

1. 概要

  2018年Bwar釋出了《C++反射機制:可變參數模闆實作C++反射》,文章非常實用,Bwar也見過好幾個看了那篇文章後以同樣方法實作反射的項目,也見過不少從我的文章抄過去連代碼風格類名函數變量名什麼都沒改或者隻是簡單改一下重新發表的。被抄說明有價值,分享出來就不在意被抄,覺得文章有用就star Nebula吧,謝謝。那些用了可變參數模闆實作反射的項目或文章大都是通過這種方法實作無參數版本的類對象建構,無參版本不能充分展現可變參數模闆實作反射的真正價值。上篇文章中關于Targ...模闆參數的說明不夠詳細且有些描述有問題,這次再寫一篇這種反射實作的補充,重點說明容易出錯的可變參數部分并糾正上篇的錯誤。畢竟在Nebula高性能網絡架構中所有actor對象的建立都必須以反射方式建立,駕馭這種反射方式建立對象讓Nebula的使用更輕松。

2. 引用折疊與類型推導

  可變參數模闆主要通過T&&引用折疊及其類型推導實作的。關于引用折疊及類型推導的說明,網上可以找到大量資料,這裡就不再贅述,推薦一篇言簡意赅清晰明了的文章《圖說函數模闆右值引用參數(T&&)類型推導規則(C++11)》。

3. 回顧一下Nebula網絡架構中的C++反射機制實作

  Nebula的Actor為事件(消息)處理者,所有業務邏輯均抽象成事件和事件處理,反射機制正是應用在Actor的動态建立上。Actor分為Cmd、Module、Step、Session四種不同類型。業務邏輯代碼均通過從這四種不同類型時間處理者派生子類來實作,專注于業務邏輯實作,而無須關注業務邏輯之外的内容。Cmd和Module都是消息處理入庫,業務開發人員定義了什麼樣的Cmd和Module對架構而言是未知的,是以這些Cmd和Module都配置在配置檔案裡,Nebula通過配置檔案中的Cmd和Module的名稱(字元串)完成它們的執行個體建立。通過反射機制動态建立Actor的關鍵代碼如下:

Actor的類聲明:

class Actor: public std::enable_shared_from_this<Actor>
           

Actor建立工廠:

template<typename ...Targs>
class ActorFactory
{
public:
    static ActorFactory* Instance()
    {
        if (nullptr == m_pActorFactory)
        {
            m_pActorFactory = new ActorFactory();
        }
        return(m_pActorFactory);
    }

    virtual ~ActorFactory(){};

    // 将“執行個體建立方法(DynamicCreator的CreateObject方法)”注冊到ActorFactory,注冊的同時賦予這個方法一個名字“類名”,後續可以通過“類名”獲得該類的“執行個體建立方法”。這個執行個體建立方法實質上是個函數指針,在C++11裡std::function的可讀性比函數指針更好,是以用了std::function。
    bool Regist(const std::string& strTypeName, std::function<Actor*(Targs&&... args)> pFunc);

    // 傳入“類名”和參數建立類執行個體,方法内部通過“類名”從m_mapCreateFunction獲得了對應的“執行個體建立方法(DynamicCreator的CreateObject方法)”完成執行個體建立操作。
    Actor* Create(const std::string& strTypeName, Targs&&... args);

private:
    ActorFactory(){};
    static ActorFactory<Targs...>* m_pActorFactory;
    std::unordered_map<std::string, std::function<Actor*(Targs&&...)> > m_mapCreateFunction;
};

template<typename ...Targs>
ActorFactory<Targs...>* ActorFactory<Targs...>::m_pActorFactory = nullptr;

template<typename ...Targs>
bool ActorFactory<Targs...>::Regist(const std::string& strTypeName, std::function<Actor*(Targs&&... args)> pFunc)
{
    if (nullptr == pFunc)
    {
        return (false);
    }
    bool bReg = m_mapCreateFunction.insert(
                    std::make_pair(strTypeName, pFunc)).second;
    return (bReg);
}

template<typename ...Targs>
Actor* ActorFactory<Targs...>::Create(const std::string& strTypeName, Targs&&... args)
{
    auto iter = m_mapCreateFunction.find(strTypeName);
    if (iter == m_mapCreateFunction.end())
    {
        return (nullptr);
    }
    else
    {
        return (iter->second(std::forward<Targs>(args)...));
    }
}
           

動态建立類:

template<typename T, typename...Targs>
class DynamicCreator
{
public:
    struct Register
    {
        Register()
        {
            char* szDemangleName = nullptr;
            std::string strTypeName;
#ifdef __GNUC__
            szDemangleName = abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, nullptr);
#else
            // 注意:這裡不同編譯器typeid(T).name()傳回的字元串不一樣,需要針對編譯器寫對應的實作
            //in this format?:     szDemangleName =  typeid(T).name();
            szDemangleName = abi::__cxa_demangle(typeid(T).name(), nullptr, nullptr, nullptr);
#endif
            if (nullptr != szDemangleName)
            {
                strTypeName = szDemangleName;
                free(szDemangleName);
            }
            ActorFactory<Targs...>::Instance()->Regist(strTypeName, CreateObject);
        }
        inline void do_nothing()const { };
    };

    DynamicCreator()
    {
        m_oRegister.do_nothing();   // 這裡的函數調用雖無實際内容,卻是在調用動态建立函數前完成m_oRegister執行個體建立的關鍵
    }
    virtual ~DynamicCreator(){};

    // 動态建立執行個體的方法,所有Actor執行個體均通過此方法建立。這是個模闆方法,實際上每個Actor的派生類都對應了自己的CreateObject方法。
    static T* CreateObject(Targs&&... args)
    {
        T* pT = nullptr;
        try
        {
            pT = new T(std::forward<Targs>(args)...);
        }
        catch(std::bad_alloc& e)
        {
            return(nullptr);
        }
        return(pT);
    }

private:
    static Register m_oRegister;
};

template<typename T, typename ...Targs>
typename DynamicCreator<T, Targs...>::Register DynamicCreator<T, Targs...>::m_oRegister;
           

  上面ActorFactory和DynamicCreator就是C++反射機制的全部實作。要完成執行個體的動态建立還需要類定義必須滿足(模闆)要求。下面看一個可以動态建立執行個體的CmdHello類定義:

// 類定義需要使用多重繼承。
// 第一重繼承neb::Cmd是CmdHello的實際基類(neb::Cmd為Actor的派生類,Actor是什麼在本節開始的描述中有說明);
// 第二重繼承為通過類名動态建立執行個體的需要,與template<typename T, typename...Targs> class DynamicCreator定義對應着看就很容易明白第一個模闆參數(CmdHello)為待動态建立的類名,其他參數為該類的構造函數參數。
// 如果參數為某個類型的引用,作為模闆參數時應指定到類型。
// 如果參數為某個類型的指針,作為模闆參數時需指定為類型的指針。
class CmdHello: public neb::Cmd, public neb::DynamicCreator<CmdHello, int32>
{
public:
    CmdHello(int32 iCmd);
    virtual ~CmdHello();

    virtual bool Init();
    virtual bool AnyMessage(
                    std::shared_ptr<neb::SocketChannel> pChannel,
                    const MsgHead& oMsgHead,
                    const MsgBody& oMsgBody);
};

           

注意:《C++反射機制:可變參數模闆實作C++反射》上篇CmdHello注釋的這兩個比如是錯誤的,具體原理見下文第5第6項

比如: 參數類型const std::string&隻需在neb::DynamicCreator的模闆參數裡填std::string

比如:參數類型const std::string則需在neb::DynamicCreator的模闆參數裡填std::string

  再看看上面的反射機制是怎麼調用的:

template <typename ...Targs>
std::shared_ptr<Cmd> WorkerImpl::MakeSharedCmd(Actor* pCreator, const std::string& strCmdName, Targs&&... args)
{
    LOG4_TRACE("%s(CmdName \"%s\")", __FUNCTION__, strCmdName.c_str());
    Cmd* pCmd = dynamic_cast<Cmd*>(ActorFactory<Targs...>::Instance()->Create(strCmdName, std::forward<Targs>(args)...));
    if (nullptr == pCmd)
    {
        LOG4_ERROR("failed to make shared cmd \"%s\"", strCmdName.c_str());
        return(nullptr);
    }
    ...
}
           

  MakeSharedCmd()方法的調用:

MakeSharedCmd(nullptr, oCmdConf["cmd"][i]("class"), iCmd);
           

4. MakeSharedActor系列函數建立對象注意事項

  這個C++反射機制的應用容易出錯的地方是:

  • 類定義class CmdHello: public neb::Cmd, public neb::DynamicCreator<CmdHello, int32>中的模闆參數一定要與構造函數中的參數類型較嚴格比對(支援隐式的類型轉換)。
  • 調用建立方法的地方傳入的實參類型必須與形參類型嚴格比對,不能有隐式的類型轉換。比如類構造函數的形參類型為unsigned int,調用ActorFactory<Targs...>::Instance()->Create()時傳入的實參為int或short或unsigned short或enum都會導緻ActorFactory無法找到對應的“執行個體建立方法”,進而導緻不能通過類名正常建立執行個體。再比如,const std::string& 與 std::string& 是不同類型,若MakeSharedActor()相關調用傳入的是std::string&,而模闆參數裡定義的是const std::string&,則調用會失敗。

  注意以上兩點,基本就不會有什麼問題。

5. 動态建立原理

  在一系列的動态建立使用案例中得出上面兩條注意事項,再從代碼中看動态建立的本質。

5.1 注冊對象建立函數指針

  首先動态建立是通過調用DynamicCreator模闆類裡的static T* CreateObject(Targs&&... args)函數來完成的。DynamicCreator模闆類在public neb::DynamicCreator<CmdHello, int32>會建立一個靜态的執行個體:

template<typename T, typename ...Targs>
typename DynamicCreator<T, Targs...>::Register DynamicCreator<T, Targs...>::m_oRegister;
           

  建立這個靜态執行個體實際上是為了 ActorFactory<Targs...>::Instance()->Regist(strTypeName, CreateObject); 注冊一個函數指針到ActorFactory,這個函數指針就是後續通過反射動态建立類對象的實際執行者。CreateObject()函數裡是調用new,傳遞的參數也是完美轉發給類的構造函數,而構造函數調用的實參與形參是支援隐式類型轉換的,是以繼承DynamicCreator時的模闆參數清單無需跟類構造函數的類型完全一緻。

5.2 動态建立的實質

  MakeSharedActor系列函數被調用,從調用的MakeSharedActor()參數是完美轉發的,沒有實參類型與形參類型的差別,也就不存在類型轉換。MakeSharedActor()裡通過調用ActorFactory<Targs...>::Instance()->Create(strCmdName, std::forward(args)...)完成建立,這個調用實際上就是由顯式的兩部分和隐含CreateObject構成:

  • ActorFactory<Targs...>::Instance() 擷取特化模闆類的一個執行個體,這一步隻要不是記憶體耗盡就一定會成功,注意這裡不是ActorFactory執行個體,而是ActorFactory<Targs..>執行個體。MakeSharedActor()調用容易讓人認為是通過類名找到對應的建立函數來動态建立對象,實際上第一步是通過調用參數的個數和類型找到對應的ActorFactory<Targs..>執行個體。
  • Create(strActorName, std::forward(args)...) 通過類名查找到對應的建立函數指針,如果找到則轉發參數給CreateObject()建立對象。沒有成功建立的絕大部分原因都是這裡找不到函數指針。通過類名查找不到對應的建立函數指針的原因是要建立對象的類沒有繼承DynamicCreator<T, Targs...>。這裡沒有繼承有明顯的沒有繼承和隐晦的未繼承,所謂隐晦的未繼承是因為調用的<Targs...>跟繼承的<Targs...>不比對,換句話說是調用和注冊不一緻:調用的ActorFactory<Targs...>執行個體并不是存儲了CreateObject函數指針的ActorFactory<Targs...>執行個體。ActorFactory<Targs...>是特化之後就是一個确定類型,不存在參數隐式轉換的可能。
  • DynamicCreator的CreateObject()函數指針調用 在第二步中被調用。隻要參數能隐式轉換成構造函數的形參類型都可以建立成功,沒有成功建立對象是因為這一步不對的可能性比較小。比如構造函數是Construct(int, int&, const std::string&),實際是CreateObject(bool, int, std::string)也是可以成功建立的。

6. 動态建立設計原則和技巧

  動态建立的參數設計的好壞直接涉及到後續動态建立是否成功和動态建立的效率(參數引用傳遞和值傳遞的差别),是以定一個設計原則很重要。

  • 從類構造函數出發,設計模闆參數類型,兩者盡可能完全一緻,若不一緻也應是無效率損失的隐式轉換。
  • 适當考慮實際調用時的參數類型作無效率損失的模闆參數調整。

  比如構造函數需要傳遞一個int型參數,模闆參數類型也設計為int,但調用方實際傳遞int&會更友善更好了解,這時可以将模闆參數類型改成int&并保持構造函數參數不變(如果将構造函數參數也改成int&會讓人誤解構造函數會改變參數的值,改成const int&又會讓調用方也改成const int&才能成功調用)。

  已定義的變量在作為實參傳遞時往往是一個T&類型,這在對象引用(比如const std::string&)時一般不會有問題,因為構造函數和模闆參數通常會設計為const std::string&,但基礎類型int、float等在構造函數和模闆參數通常是以值傳遞的,這時候就涉及到上面舉例的int&的情景,如果不想調整模闆參數類型,還有一個小技巧是在傳遞的實參前面加上(int)、(float)做一個強轉,強轉後參數變成按值傳遞就可以調用到正确的建立函數。僞代碼如下:

// class Test : public neb::Actor, public neb::DynamicCreator<Test, int&, int&, std::string&>
class Test : public neb::Actor, public neb::DynamicCreator<Test, int, int, std::string&>       // 注意模闆參數類型std::string&,而構造函數的參數類型為const std::string&
{
public:
    Test(int iFrom, int iTo, const std::string& strName);
    ...
};

int main()
{
    int iFrom = 0;
    int iTo = 500;
    std::string strName = "latency";    // 若上面模闆參數類型改為const std::string&,則這裡需改成 const std::string strName = "latency";
    MakeSharedActor("Test", iFrom, iTo, strName);    // 調用失敗
    MakeSharedActor("Test", (int)iFrom, (int)iTo, strName);     // 調用成功
}
           

  如果覺得文章有用就star Nebula吧,謝謝。

作者:Bwar

出處:https://www.cnblogs.com/bwar/

Bwar傾力打造的高性能網絡架構Nebula:https://github.com/Bwar/Nebula

原創文章如轉載,請注明出處。

繼續閱讀