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的關鍵代碼以下:ide

Actor的類聲明:函數

class Actor: public std::enable_shared_from_this

Actor建立工廠:性能

templateclass 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::functionpFunc);

    // 傳入「類名」和參數建立類實例,方法內部經過「類名」從m_mapCreateFunction得到了對應的「實例建立方法(DynamicCreator的CreateObject方法)」完成實例建立操做。
    Actor* Create(const std::string& strTypeName, Targs&&... args);

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

templateActorFactory* ActorFactory::m_pActorFactory = nullptr;

templatebool ActorFactory::Regist(const std::string& strTypeName, std::functionpFunc)
{
    if (nullptr == pFunc)
    {
        return (false);
    }
    bool bReg = m_mapCreateFunction.insert(
                    std::make_pair(strTypeName, pFunc)).second;
    return (bReg);
}

templateActor* ActorFactory::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(args)...));
    }
}

動態建立類:this

templateclass 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::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(args)...);
        }
        catch(std::bad_alloc& e)
        {
            return(nullptr);
        }
        return(pT);
    }

private:
    static Register m_oRegister;
};

templatetypename DynamicCreator::Register DynamicCreator::m_oRegister;

  上面ActorFactory和DynamicCreator就是C++反射機制的所有實現。要完成實例的動態建立還須要類定義必須知足(模板)要求。下面看一個能夠動態建立實例的CmdHello類定義:設計

// 類定義須要使用多重繼承。
// 第一重繼承neb::Cmd是CmdHello的實際基類(neb::Cmd爲Actor的派生類,Actor是什麼在本節開始的描述中有說明);
// 第二重繼承爲經過類名動態建立實例的須要,與templateclass DynamicCreator定義對應着看就很容易明白第一個模板參數(CmdHello)爲待動態建立的類名,其餘參數爲該類的構造函數參數。
// 若是參數爲某個類型的引用,做爲模板參數時應指定到類型。
// 若是參數爲某個類型的指針,做爲模板參數時需指定爲類型的指針。
class CmdHello: public neb::Cmd, public neb::DynamicCreator{
public:
    CmdHello(int32 iCmd);
    virtual ~CmdHello();

    virtual bool Init();
    virtual bool AnyMessage(
                    std::shared_ptrpChannel,
                    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指針

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

templatestd::shared_ptrWorkerImpl::MakeSharedCmd(Actor* pCreator, const std::string& strCmdName, Targs&&... args)
{
    LOG4_TRACE("%s(CmdName \"%s\")", __FUNCTION__, strCmdName.c_str());
    Cmd* pCmd = dynamic_cast(ActorFactory::Instance()->Create(strCmdName, std::forward(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
  • 調用建立方法的地方傳入的實參類型必須與形參類型嚴格匹配,不能有隱式的類型轉換。好比類構造函數的形參類型爲unsigned int,調用ActorFactory

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

5. 動態建立原理

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

5.1 註冊對象建立函數指針

  首先動態建立是經過調用DynamicCreator模板類裏的static T* CreateObject(Targs&&... args)函數來完成的。DynamicCreator模板類在public neb::DynamicCreator

templatetypename DynamicCreator::Register DynamicCreator::m_oRegister;

  建立這個靜態實例其實是爲了 ActorFactory

5.2 動態建立的實質

  MakeSharedActor系列函數被調用,從調用的MakeSharedActor()參數是完美轉發的,沒有實參類型與形參類型的區別,也就不存在類型轉換。MakeSharedActor()裏經過調用ActorFactory

  • ActorFactory 獲取特化模板類的一個實例,這一步只要不是內存耗盡就必定會成功,注意這裏不是ActorFactory實例,而是ActorFactory
  • Create(strActorName, std::forward 經過類名查找到對應的建立函數指針,若是找到則轉發參數給CreateObject()建立對象。沒有成功建立的絕大部分緣由都是這裏找不到函數指針。經過類名查找不到對應的建立函數指針的緣由是要建立對象的類沒有繼承DynamicCreator
  • 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::DynamicCreatorclass Test : public neb::Actor, public neb::DynamicCreator// 注意模板參數類型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吧,謝謝。

相關文章
相關標籤/搜索