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吧,謝謝。