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

C++反射机制:可变参数模板实现C++反射(二)的更多相关文章

  1. C++反射机制:可变参数模板实现C++反射

    1. 概要   本文描述一个通过C++可变参数模板实现C++反射机制的方法.该方法非常实用,在Nebula高性能网络框架中大量应用,实现了非常强大的动态加载动态创建功能.Nebula框架在Github ...

  2. C++反射机制:可变参数模板实现C++反射(使用C++11的新特性--可变模版参数,只根据类的名字(字符串)创建类的实例。在Nebula高性能网络框架中大量应用)

    1. 概要   本文描述一个通过C++可变参数模板实现C++反射机制的方法.该方法非常实用,在Nebula高性能网络框架中大量应用,实现了非常强大的动态加载动态创建功能.Nebula框架在码云的仓库地 ...

  3. Java反射机制demo(七)—反射机制与工厂模式

    Java反射机制demo(七)—反射机制与工厂模式 工厂模式 简介 工厂模式是最常用的实例化对象模式. 工厂模式的主要作用就是使用工厂方法代替new操作. 为什么要使用工厂模式?直接new不好吗? 直 ...

  4. java中的反射机制,以及如何通过反射获取一个类的构造方法 ,成员变量,方法,详细。。

    首先先说一下类的加载,流程.只有明确了类这个对象的存在才可以更好的理解反射的原因,以及反射的机制. 一.  类的加载 当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三 ...

  5. java反射机制 + Method.invoke解释 getMethod + 反射理解

    功能: 通过读取另一个Dll去创建一个控件(Form,Button,TextBox,DataGridView),然后对当中一些属性进行检查. 创建控件的大致流程是,Assembly->Modul ...

  6. Android反射机制:手把手教你实现反射

    什么是反射机制? JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取的信息以及动态调用对象的方法的功能称 ...

  7. java基础知识(十一)java反射机制(下)

    1.什么是反射机制? java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象都能够调用他的属性和方法,这种动态获取属性和方法的功能称为java的反射机制. ...

  8. ReflectUitls类的编写和对反射机制的解析

    ReflectUitls类的编写和对反射机制的解析 反射相关的类 反射相关的类,最基本的当然是Class类. 获取了Class对象之后,就可以接着生成对象实例.调用方法.查看字段等等. 字段(Fiel ...

  9. java的反射机制

    一.java的反射机制浅谈 最近研究java研究得很给力,主要以看博文为学习方式.以下是我对java的反射机制所产生的一些感悟,希望各位童鞋看到失误之处不吝指出.受到各位指教之处,如若让小生好好感动, ...

随机推荐

  1. JWT安装配置

    1.1 安装JWT pip install djangorestframework-jwt==1.11.0 1.2 syl/settings.py 配置jwt载荷中的有效期设置 import date ...

  2. 记一次微信公众号(微信H5)网页对接腾讯云慧眼人脸核验开发笔记

    需求是这样的,客户需要一个在观看学习视频前弹出人脸识别核验真人的功能,客户找了个APP作为参考,但是在微信上第三方人脸识别是无法直接调取到前置摄像头的,都是通过用户自己获取用户的身份信息或者照片,然后 ...

  3. LWJGL3的内存管理

    LWJGL3的内存管理 LWJGL3 (Lightweight Java Game Library 3),是一个支持OpenGL,OpenAl,Opengl ES,Vulkan等的Java绑定库.&l ...

  4. ThinkPHP中,运用PHPExcel,将数据库导出到Excel中

    1.将PHPExcel插件放在项目中,本人位置是ThinkPHP文件夹下,目录结构如下/ThinkPHP/Library//Vendor/...2.直接根据模型,配置三个变量即可使用./** * Ex ...

  5. hadoop使用实例

    一.词频统计 1.下载喜欢的电子书或大量文本数据,并保存在本地文本文件中 2.编写map与reduce函数 3.本地测试map与reduce 4.将文本数据上传至HDFS上 5.用hadoop str ...

  6. 已经编译安装的nginx/tenginx编译增加新模块

    只适用于自行编译安装的nginx配置 业务变更带来的Nginx增加模块需求 由于业务从php转为go开发,需要用到Http2的协议.这种协议在Nginx上需要http_v2_module这个模块的支持 ...

  7. Mybatis的基础配置

    mybatis相关配置 mybatis核心文件配置的用法以及事例(初级) properties标签:获取外部的配置文件 <properties resource="jdbc.prope ...

  8. Simulink代码自动生成(一)

    前面介绍了MDB的设计思想并对比了它和传统的嵌入式软件开发之间的差异,现在开始使用Simulink工具演示模型的搭建和C代码的自动生成过程. Matlab版本:R2018B 一.算法模型的搭建 搭建一 ...

  9. nginx下配置php5和php7

    用的是lnmp 一键安装的 php5.6版本网上百度Ubuntu安装多版本PHP就行 参考文章原链接:http://blog.csdn.net/21aspnet/article/details/476 ...

  10. 【转载】图解Transformer(完整版)!

    在学习深度学习过程中很多讲的不够细致,这个讲的真的是透彻了,转载过来的,希望更多人看到(转自-张贤同学-公众号). 前言 本文翻译自 http://jalammar.github.io/illustr ...