C++反射機制:可變引數模板實現C++反射(二)

鐵芒箕發表於2020-11-29

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

相關文章