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项
再看看上面的反射机制是怎么调用的:
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
- ActorFactory<Targs...>::Instance() 获取特化模板类的一个实例,这一步只要不是内存耗尽就一定会成功,注意这里不是ActorFactory实例,而是ActorFactory<Targs..>实例。MakeSharedActor()调用容易让人认为是通过类名找到对应的创建函数来动态创建对象,实际上第一步是通过调用参数的个数和类型找到对应的ActorFactory<Targs..>实例。
- Create(strActorName, std::forward 通过类名查找到对应的创建函数指针,如果找到则转发参数给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吧,谢谢。