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的关键代码如下:
class Actor: public std::enable_shared_from_this<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++反射(二)的更多相关文章
- C++反射机制:可变参数模板实现C++反射
1. 概要 本文描述一个通过C++可变参数模板实现C++反射机制的方法.该方法非常实用,在Nebula高性能网络框架中大量应用,实现了非常强大的动态加载动态创建功能.Nebula框架在Github ...
- C++反射机制:可变参数模板实现C++反射(使用C++11的新特性--可变模版参数,只根据类的名字(字符串)创建类的实例。在Nebula高性能网络框架中大量应用)
1. 概要 本文描述一个通过C++可变参数模板实现C++反射机制的方法.该方法非常实用,在Nebula高性能网络框架中大量应用,实现了非常强大的动态加载动态创建功能.Nebula框架在码云的仓库地 ...
- Java反射机制demo(七)—反射机制与工厂模式
Java反射机制demo(七)—反射机制与工厂模式 工厂模式 简介 工厂模式是最常用的实例化对象模式. 工厂模式的主要作用就是使用工厂方法代替new操作. 为什么要使用工厂模式?直接new不好吗? 直 ...
- java中的反射机制,以及如何通过反射获取一个类的构造方法 ,成员变量,方法,详细。。
首先先说一下类的加载,流程.只有明确了类这个对象的存在才可以更好的理解反射的原因,以及反射的机制. 一. 类的加载 当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三 ...
- java反射机制 + Method.invoke解释 getMethod + 反射理解
功能: 通过读取另一个Dll去创建一个控件(Form,Button,TextBox,DataGridView),然后对当中一些属性进行检查. 创建控件的大致流程是,Assembly->Modul ...
- Android反射机制:手把手教你实现反射
什么是反射机制? JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法和属性:这种动态获取的信息以及动态调用对象的方法的功能称 ...
- java基础知识(十一)java反射机制(下)
1.什么是反射机制? java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象都能够调用他的属性和方法,这种动态获取属性和方法的功能称为java的反射机制. ...
- ReflectUitls类的编写和对反射机制的解析
ReflectUitls类的编写和对反射机制的解析 反射相关的类 反射相关的类,最基本的当然是Class类. 获取了Class对象之后,就可以接着生成对象实例.调用方法.查看字段等等. 字段(Fiel ...
- java的反射机制
一.java的反射机制浅谈 最近研究java研究得很给力,主要以看博文为学习方式.以下是我对java的反射机制所产生的一些感悟,希望各位童鞋看到失误之处不吝指出.受到各位指教之处,如若让小生好好感动, ...
随机推荐
- Java到处运行的基础之 Class 文件
Java 实现一次编译到处运行的基础,来源于 Java 虚拟机屏蔽了操作系统的底层细节.使用 class 文件存储编译后的源程序,使得 Java 程序的编译与操作系统解耦.正是因为 Java clas ...
- php socket通信的简单实现
socket通信的原理在这里就不说了,它的用途还是比较广泛的,我们可以使用socket来做一个API接口出来,也可以使用socket来实现两个程序之间的通信,我们来研究一下在php里面如何实现sock ...
- 05 . Go+Vue开发一个线上外卖应用(Session集成及修改用户头像到Fastdfs)
用户头像上传 功能介绍 在用户中心中,允许用户更换自己的头像.因此,我们开发上传一张图片到服务器,并保存成为用户的头像. 接口解析 在用户模块的控制器MemberController中,解析头像上传的 ...
- 关于DevOps的七大误解,99%的人都曾中过招!
[摘要] DevOps方法可以为组织带来显著的积极影响,降低成本.提高效率,使开发团队的工作更加精简.为了掌握这个过程的优势,有必要认识到DevOps是什么.不是什么.在本文中,就将讨论一些流传甚广的 ...
- Python3网络学习案例一:Ping详解
1. 使用Ping做什么 ping用于确定本地主机是否能与另一台主机成功交换(发送与接收)数据包,再根据返回的信息,就可以推断TCP/IP参数是否设置正确,以及运行是否正常.网络是否通畅等. 2. 效 ...
- CH2101可达性问题
CH2101可达性问题 拓扑排序应用基础 题意描述 具体见书P95. 给定一个N个点,M条边的有向无环图,问每个点直接或间接可到达的点的数量. 算法分析 书中有详细介绍,这里就不再赘述了. 简而言之就 ...
- CopyOnWriteArrayList线程安全分析
CopyOnWriteArrayList是开发过程中常用的一种并发容器,多用于读多写少的并发场景.但是CopyOnWriteArrayList真的能做到完全的线程安全吗? 答案是并不能. 一.Copy ...
- 辨析:IIR(Infinite Impulse Response)与FIR(Finite Impulse Response)
IIR和FIR系统描述的是系统的抽样响应特性,其中$ x(n)=\delta(n) $ 举例:一个平均器的系统是FIR系统,因为它的抽样响应仅在变量n取某3个值的时候有值,是有限长的:一阶自回归模型由 ...
- Java_基础(二)
思想 面向过程的思想: 怎么按步骤把问题解决, 并将步骤编程方法, 一步一步事项, 适合简单不需要协作的任务 面向对象的思想: 怎么设计这个事务 区别与联系 都是解决问题的思维方式, 都是代码组织的方 ...
- Spring Cloud Alibaba 之 user服务
项目技术选型 Spring Boot Spring MVC MyBatis + 通用Mapper (官网信息https://mapperhelper.github.io/docs/) Spring C ...