请注意,这不是最佳解决方案(有关问题,请参阅文章底部),而是一种结合模板和虚函数的可行方法。我发布它是希望您可以以此为基础提出更有效的方法。如果您找不到改进的方法,我建议按照the other answer 的建议模板化Entity。
如果你不想对Entity做任何大的修改,你可以在World中实现一个隐藏的虚拟助手函数,来实际创建组件。在这种情况下,辅助函数可以接受一个参数,该参数指示要构造什么样的组件,并返回void*; createComponent() 调用隐藏函数,指定ComponentType,并将返回值转换为ComponentType*。我能想到的最简单的方法是给每个组件一个静态成员函数create(),并将类型索引映射到create() 调用。
为了让每个组件接受不同的参数,我们可以使用辅助类型,我们称之为Arguments。这种类型在包装实际参数列表的同时提供了一个简单的接口,使我们能够轻松地定义我们的create() 函数。
// Argument helper type. Converts arguments into a single, non-template type for passing.
class Arguments {
public:
struct ArgTupleBase
{
};
template<typename... Ts>
struct ArgTuple : public ArgTupleBase {
std::tuple<Ts...> args;
ArgTuple(Ts... ts) : args(std::make_tuple(ts...))
{
}
// -----
const std::tuple<Ts...>& get() const
{
return args;
}
};
// -----
template<typename... Ts>
Arguments(Ts... ts) : args(new ArgTuple<Ts...>(ts...)), valid(sizeof...(ts) != 0)
{
}
// -----
// Indicates whether it holds any valid arguments.
explicit operator bool() const
{
return valid;
}
// -----
const std::unique_ptr<ArgTupleBase>& get() const
{
return args;
}
private:
std::unique_ptr<ArgTupleBase> args;
bool valid;
};
接下来,我们将组件定义为具有create() 函数,该函数接受const Arguments& 并从中获取参数,方法是调用get(),取消引用指针,将指向的ArgTuple<Ts...> 转换为匹配组件的构造函数参数列表,最后得到get()的实际参数元组。
请注意,如果 Arguments 是使用不正确的参数列表(与组件的构造函数的参数列表不匹配)构造的,这将失败,就像直接使用不正确的参数列表调用构造函数一样;它将接受一个空的参数列表,但是,由于Arguments::operator bool(),允许提供默认参数。 [不幸的是,目前,此代码存在类型转换问题,特别是当类型大小不同时。我还不确定如何解决这个问题。]
// Two example components.
class One {
int i;
bool b;
public:
One(int i, bool b) : i(i), b(b) {}
static void* create(const Arguments& arg_holder)
{
// Insert parameter types here.
auto& args
= static_cast<Arguments::ArgTuple<int, bool>&>(*(arg_holder.get())).get();
if (arg_holder)
{
return new One(std::get<0>(args), std::get<1>(args));
}
else
{
// Insert default parameters (if any) here.
return new One(0, false);
}
}
// Testing function.
friend std::ostream& operator<<(std::ostream& os, const One& one)
{
return os << "One, with "
<< one.i
<< " and "
<< std::boolalpha << one.b << std::noboolalpha
<< ".\n";
}
};
std::ostream& operator<<(std::ostream& os, const One& one);
class Two {
char c;
double d;
public:
Two(char c, double d) : c(c), d(d) {}
static void* create(const Arguments& arg_holder)
{
// Insert parameter types here.
auto& args
= static_cast<Arguments::ArgTuple<char, double>&>(*(arg_holder.get())).get();
if (arg_holder)
{
return new Two(std::get<0>(args), std::get<1>(args));
}
else
{
// Insert default parameters (if any) here.
return new Two('\0', 0.0);
}
}
// Testing function.
friend std::ostream& operator<<(std::ostream& os, const Two& two)
{
return os << "Two, with "
<< (two.c == '\0' ? "null" : std::string{ 1, two.c })
<< " and "
<< two.d
<< ".\n";
}
};
std::ostream& operator<<(std::ostream& os, const Two& two);
然后,有了所有这些,我们终于可以实现Entity、World 和WorldImpl。
// This is the world interface.
class World
{
// Actual worker.
virtual void* create_impl(const std::type_index& ctype, const Arguments& arg_holder) = 0;
// Type-to-create() map.
static std::unordered_map<std::type_index, std::function<void*(const Arguments&)>> creators;
public:
// Templated front-end.
template<typename ComponentType>
ComponentType* createComponent(const Arguments& arg_holder)
{
return static_cast<ComponentType*>(create_impl(typeid(ComponentType), arg_holder));
}
// Populate type-to-create() map.
static void populate_creators() {
creators[typeid(One)] = &One::create;
creators[typeid(Two)] = &Two::create;
}
};
std::unordered_map<std::type_index, std::function<void*(const Arguments&)>> World::creators;
// Just putting in a dummy parameter for now, since this simple example doesn't actually use it.
template<typename Allocator = std::allocator<World>>
class WorldImpl : public World
{
void* create_impl(const std::type_index& ctype, const Arguments& arg_holder) override
{
return creators[ctype](arg_holder);
}
};
class Entity
{
World* world;
public:
template<typename ComponentType, typename... Args>
void assign(Args... args)
{
ComponentType* component = world->createComponent<ComponentType>(Arguments(args...));
std::cout << *component;
delete component;
}
Entity() : world(new WorldImpl<>())
{
}
~Entity()
{
if (world) { delete world; }
}
};
int main() {
World::populate_creators();
Entity e;
e.assign<One>();
e.assign<Two>();
e.assign<One>(118, true);
e.assign<Two>('?', 8.69);
e.assign<One>('0', 8); // Fails; calls something like One(1075929415, true).
e.assign<One>((int)'0', 8); // Succeeds.
}
看到它在行动here。
也就是说,这有几个问题:
-
create_impl() 依赖于typeid,失去了编译时类型推导的好处。与模板化相比,这会导致执行速度变慢。
- 使问题更加复杂的是,
type_info 没有 constexpr 构造函数,即使 typeid 参数是 LiteralType 时也没有。
- 我不确定如何从
Argument 获取实际的ArgTuple<Ts...> 类型,而不仅仅是铸造和祈祷。这样做的任何方法都可能取决于 RTTI,我想不出一种方法来使用它来映射 type_indexes 或任何类似于不同模板专业化的东西。
- 因此,参数必须在
assign() 调用点进行隐式转换或强制转换,而不是让类型系统自动完成。这……有点问题。