百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

C++反射机制:可变参数模板实现C++反射(二)

ztj100 2025-01-21 23:12 21 浏览 0 评论

本文来自博客园:Bwar

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);     // 调用成功

想学习C++,可以点击了解更多,C++视频免费学习

相关推荐

其实TensorFlow真的很水无非就这30篇熬夜练

好的!以下是TensorFlow需要掌握的核心内容,用列表形式呈现,简洁清晰(含表情符号,<300字):1.基础概念与环境TensorFlow架构(计算图、会话->EagerE...

交叉验证和超参数调整:如何优化你的机器学习模型

准确预测Fitbit的睡眠得分在本文的前两部分中,我获取了Fitbit的睡眠数据并对其进行预处理,将这些数据分为训练集、验证集和测试集,除此之外,我还训练了三种不同的机器学习模型并比较了它们的性能。在...

机器学习交叉验证全指南:原理、类型与实战技巧

机器学习模型常常需要大量数据,但它们如何与实时新数据协同工作也同样关键。交叉验证是一种通过将数据集分成若干部分、在部分数据上训练模型、在其余数据上测试模型的方法,用来检验模型的表现。这有助于发现过拟合...

深度学习中的类别激活热图可视化

作者:ValentinaAlto编译:ronghuaiyang导读使用Keras实现图像分类中的激活热图的可视化,帮助更有针对性...

超强,必会的机器学习评估指标

大侠幸会,在下全网同名[算法金]0基础转AI上岸,多个算法赛Top[日更万日,让更多人享受智能乐趣]构建机器学习模型的关键步骤是检查其性能,这是通过使用验证指标来完成的。选择正确的验证指...

机器学习入门教程-第六课:监督学习与非监督学习

1.回顾与引入上节课我们谈到了机器学习的一些实战技巧,比如如何处理数据、选择模型以及调整参数。今天,我们将更深入地探讨机器学习的两大类:监督学习和非监督学习。2.监督学习监督学习就像是有老师的教学...

Python教程(三十八):机器学习基础

...

Python 模型部署不用愁!容器化实战,5 分钟搞定环境配置

你是不是也遇到过这种糟心事:花了好几天训练出的Python模型,在自己电脑上跑得顺顺当当,一放到服务器就各种报错。要么是Python版本不对,要么是依赖库冲突,折腾半天还是用不了。别再喊“我...

超全面讲透一个算法模型,高斯核!!

...

神经网络与传统统计方法的简单对比

传统的统计方法如...

AI 基础知识从0.1到0.2——用“房价预测”入门机器学习全流程

...

自回归滞后模型进行多变量时间序列预测

下图显示了关于不同类型葡萄酒销量的月度多元时间序列。每种葡萄酒类型都是时间序列中的一个变量。假设要预测其中一个变量。比如,sparklingwine。如何建立一个模型来进行预测呢?一种常见的方...

苹果AI策略:慢哲学——科技行业的“长期主义”试金石

苹果AI策略的深度原创分析,结合技术伦理、商业逻辑与行业博弈,揭示其“慢哲学”背后的战略智慧:一、反常之举:AI狂潮中的“逆行者”当科技巨头深陷AI军备竞赛,苹果的克制显得格格不入:功能延期:App...

时间序列预测全攻略,6大模型代码实操

如果你对数据分析感兴趣,希望学习更多的方法论,希望听听经验分享,欢迎移步宝藏公众号...

AI 基础知识从 0.4 到 0.5—— 计算机视觉之光 CNN

...

取消回复欢迎 发表评论: