C++ ECS架构ForEach实现

本文记录了作者在C++中尝试模仿Unity的Entities.ForEach接口所遇到的挑战,包括组件类型ID绑定、lambda参数提取、成员函数级联调用和重载等。作者通过模板编程和类型萃取解决了这些问题,详细介绍了如何利用模板类匹配不同类型的函数、成员函数和lambda表达式,并实现了可变参数的支持。文章还涉及了为参数分配ID以及限制lambda表达式参数数量的方法,最后展示了测试代码。
摘要由CSDN通过智能技术生成

最近参考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;
}

搞定!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值