最近参考unity的ecs架构,想写一个C++版本给自己的demo使用。在写到ForEach的时候,发现要想实现类似unity的ForEach还是要花点功夫的,因此写一篇文章记录一下。先看一下unity版本这个接口怎么用。
Entities
.WithStoreEntityQueryInField(ref query)
.ForEach(
(ref RotationQuaternion rotation, in RotationSpeed speed) => {
rotation.Value
= math.mul(
math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(),
speed.RadiansPerSecond * deltaTime)
);
})
要实现上面的接口形式需要解决几个问题:
1.组件类型的id绑定
2.lambda表达式的参数萃取
3.类成员函数的级联调用
4.lambda表达式调用函数的重载
解决以上几个问题的武器就是C++的模板编程,因此我会从简入深的一步一步解决上面的问题。
从哪里说起呢。。。就从变量的型别说起吧。。。一个变量无论是内置类型还是类类型,在C++里有三种型别表示,分别是原本类型,指针类型,引用类型。一个函数也可以理解成一个变量,那么它也就具备三种型别表示,分别是函数原本类型,函数指针,函数引用。
//函数原型
int AddSelf(int value)
{
return value++;
}
//函数指针
using AddSelfPtr = int(*)(int);
//函数引用
using AddSelfRef = int(&)(int);
现在我们希望将上面的函数模板化,我们该如何让这三种类型准确匹配呢?这里我们就需要使用模板类的偏特化了(记住模板函数没有偏特化,只有全特化,因为C++标志委员会说不需要函数模板的偏特化,因为函数有函数重载机制)。
template <typename T>
struct FuctionTraits;
//int(*)(int)
template <typename R, typename P>
struct FuctionTraits<R(*)(P)>
{
//这个函数每什么意义,纯粹为了调试看进入哪个类中,注意不要开优化。。。
void Excute()
{
int i = 0;
}
};
//int(&)(int)
template <typename R, typename P>
struct FuctionTraits<R(&)(P)>
{
void Excute()
{
int i = 0;
}
};
//int(int)
template <typename R, typename P>
struct FuctionTraits<R(P)>
{
void Excute()
{
int i = 0;
}
};
//int(int)
FuctionTraits<decltype(AddSelf)> ft;
ft.Excute();
//int(*)(int)
FuctionTraits<AddSelfPtr> ftp;
ftp.Excute();
//int(&)(int)
FuctionTraits<AddSelfRef> ftr;
ftr.Excute();
首先我们定义了一个模板类FuctionTraits它有一个模板形参,接下来我们特化了三种类型,这里FuctionTraits的模板形参T,需要使用特化版本的两个模板形参推导出来,R代表返回值,P代码一个函数形参。可以看到每一种形式都能正确的匹配,decltype是C++11新引入的关键字,它可以返回()中的型别,注意和auto型别推导的区别,decltype会照葫芦画瓢返回型别,不会做任何猫腻,比如有const就会保留const修饰符。接下来我们再深入一下,去匹配类的成员函数。
template <typename R, typename C, typename P>
struct FuctionTraits<R(C::*)(P)>
{
void Excute()
{
int i = 0;
}
};
template <typename R, typename C, typename P>
struct FuctionTraits<R(C::*)(P) const>
{
void Excute()
{
int i = 0;
}
};
struct TestClass
{
int AddSelf(int value)
{
return value++;
}
int AddSelf(int value) const
{
return value++;
}
};
//类成员函数指针
using ClassMemberFuctionConstPtr = int(TestClass::*)(int) const;
using ClassMemberFuctionPtr = int(TestClass::*)(int);
ClassMemberFuctionConstPtr cptr = &TestClass::AddSelf;
FuctionTraits<decltype(cptr)> fcptr;
fcptr.Excute();
FuctionTraits<ClassMemberFuctionPtr> fptr;
fptr.Excute();
首先我们要引入类的类型,因此整理了一个模板形参C,另外注意const成员函数和非const成员函数是两种不同的重载版本。接下来我们匹配lambda表达式。
template <typename R, typename C, typename P>
struct FuctionTraits<R(C::*)(P) const>
{
void Excute()
{
int i = 0;
}
};
template <typename T>
struct FuctionTraits : public FuctionTraits<decltype(&T::operator())> {};
auto lambda = [](int value) {return value++; };
FuctionTraits<decltype(lambda)> fl;
fl.Excute();
上面的代码看上去有点奇怪了,我尝试解释一下,lambda表达式可以简单理解为匿名函数,它的实现类似于仿函数(一个类重载了operator()操作符的类),那么也就可以理解lambda表达式其实是一个类的成员函数,而这个成员函数就是operator()。要想准确匹配lambda表达式我们首先要获得lambda匿名函数的类型,获得的方式就是&decltype(lambda)::operator(),decltype(lambda)就是匿名对象的类,&decltype(lambda)::operator()就是operator()函数。上面的代码,我使用了模板类的继承来推导lambda表达式的类型。也可以写成下面的形式,理解起来更容易。注意一个lambda函数是一个const成员函数,为什么是这样,i don't konw...
auto lambda = [](int value) {return value++; };
FuctionTraits<decltype(&decltype(lambda)::operator())> fl;
fl.Excute();
Ok,至此我们已经可以通过模板类准确匹配lambda表达式了,这是最重要的一步接下来我们要让模板类支持可变参数,这个很简单,只需要将模板参数变为可变参数。
template <typename R, typename C, typename... P>
struct FuctionTraits<R(C::*)(P...) const>
{
void Excute()
{
int i = 0;
}
};
//无论使用多少个参数都可以准确的匹配模板类
auto lambda = [](int value, double value1) {return value++; };
此时无论你怎么修改lambda表达式的参数,都可以准确的匹配模板类。接下来我们就需要在模板类中实现我们所需的功能,第一个功能就是萃取函数的参数类型。我们可以使用std::tuple_element_t来萃取每一个参数,std::tuple_element_t实现原理就是递归调用args,然后获取每一层的类型。为此我添加了一个helpler类来萃取参数类型。
template <typename R, typename... Args>
struct FuctionTraitsHelper
{
//取得参数个数
static constexpr auto param_count = sizeof...(Args);
using return_type = R;
//萃取第N个参数
template <std::size_t N>
using param_type = std::tuple_element_t<N, std::tuple<Args...>>;
};
//先推导父类
template <typename R, typename C, typename... Args>
struct FuctionTraits<R(C::*)(Args...) const> : FuctionTraitsHelper<R, Args...> {};
如果我们需要萃取lambda表达式中所有的参数,我们还需要一个递归类。
template<typename T, UINT32 N>
struct LambdaTraits
{
static void Traits(std::vector<LambdaParamInfo>& lambdaInfoVec){
using paramType = FuctionTraits<T>::param_type<N - 1>;
}
};
template<typename T>
struct LambdaTraits<T, 1>
{
static void Traits(std::vector<LambdaParamInfo>& lambdaInfoVec){
using paramType = FuctionTraits<T>::param_type<0>;
}
};
template<typename T>
void ComponentTraitsFormLambda(T Lambda)
{
LambdaTraits<T, FuctionTraits<T>::param_count>::Traits();
}
接下来我们需要为每一个类型分配id,我使用函数模板的特化为每一个类分配id,这个id需要手动注册,我没有实现自动注册id。
class TypeIndex
{
public:
template<typename T>
static constexpr UINT32 GetId();
};
#define RegisterTypeId(type, id) \
template<> \
static constexpr UINT32 TypeIndex::GetId<type>()\
{ \
return id; \
}; \
RegisterTypeId(Entity, 0)
RegisterTypeId(Translation, 10000)
RegisterTypeId(double, 10005)
//获取第0个参数的类型ID
using paramType = FuctionTraits<T>::param_type<0>;
TypeIndex::GetId<std::remove_const_t<std::remove_reference_t<paramType>>>;
另外我没有特化const以及引用版本,因此我在获取id的时候需要去掉const和引用,至此对于lambda表达式类型的萃取工作基本完成了。在编写的过程中,我尝试萃取参数是否是const类型,发现这里有几个坑,记录一下。
1.如果函数的形参既不是引用,也不是指针类型 ,那么函数的形参的const属性会被退化,因此参数会被复制一份保证了const的特性,因此无论你怎么萃取都萃取不到const。
2.std::is_const萃取引用或指针型别的时候会退化const,因此萃取的时候需要去掉引用和指针。
3.基于以上两点我强制ForEach的lambda表达式的形参类型必须是引用,这样的好处是第一提高形参传递的速度,第二我可以准确判断const特性。
最后我们需要实现成员函数的级联以及lambda的回调。级联很容易实现,只需要将函数的返回值设置成类的引用。lambda的回调需要写一个switch根据参数的个数进行回调,也就是说对使用者来说lambda的参数有限制,我简单看了一下unity的实现,对于lambda函数的参数也是有限制的,它是通过函数重载实现的。在我目前的认知里面调用函数这个操作是无法自动实现的。如果这个实现了,那么真的就可以程序自己编程自己了。
class Entitys
{
public:
template<typename T>
Entitys& ForEach(T lambda)
{
ComponentTraitsFormLambda(lambda, mLambdaParamInfoVec);
UINT32 paramCount = mLambdaParamInfoVec.size();
switch (paramCount)
{
case 1:
lambda(Entity());
case 2:
lambda(Entity(), Translation());
case 3:
default:
break;
}
return *this;
}
private:
std::vector<LambdaParamInfo> mLambdaParamInfoVec;
};
最后我们测试一下
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance, PSTR cmdLine, int showCmd)
{
SFire::Entitys test;
test.ForEach([](SFire::Entity& entity, SFire::Translation& trans) {})
.Run();
return 0;
}
搞定!