我不确定如何最好地表达这个问题,但我不是问如何实现模板化的虚函数本身。我正在构建一个实体组件系统,我有两个重要的类 - World
和Entity
。 World实际上是一个抽象类,实现(让我们称之为WorldImpl
)是一个模板化的类,允许使用自定义分配器(可以与std::allocator_traits
一起使用)。
组件是我们可以附加到实体的任何数据类型。这是通过在实体上调用名为assign
的模板化函数来完成的。
问题在于:我在创建和初始化组件时尝试让实体使用世界的分配器。在一个完美的世界中,你会调用Entity::assign<ComponentType>( ... )
,它会要求WorldImpl
创建具有适当分配器的组件。但是这里有一个问题 - 实体有一个指向World
的指针,根据我的知识,模板化的虚函数是不可能的。
以下是一个可能使问题更加明显的插图:
class Entity
{
template<typename ComponentType>
void assign(/* ... */)
{
/* ... */
ComponentType* component = world->createComponent<ComponentType>(/* ... */);
/* ... */
}
World* world;
};
// This is the world interface.
class World
{
// This is the ideal, which isn't possible as it would require templated virtual functions.
template<typename ComponentType>
virtual ComponentType* createComponent(/* ... */) = 0;
};
template<typename Allocator>
class WorldImpl : public World
{
template<typename ComponentType> // again, not actually possible
virtual ComponentType* createComponent(/* ... */)
{
// do something with Allocator and ComponentType here
}
};
看到上面的代码实际上是不可能的,这是真正的问题:对于像这样的类层次结构,我需要做什么黑魔法才能使用ComponentType和Allocator模板调用某些函数参数?这是最终目标 - 一个函数调用某个对象,并且可以使用两个模板参数。
答案 0 :(得分:2)
我会说实体属于某种世界,并使用World参数制作模板。然后你可以忘记所有的继承和virtual
,只需实现满足所需接口的世界,例如。
template<typename World>
class Entity
{
template<typename ComponentType>
void assign(/* ... */)
{
/* ... */
ComponentType* component = world.createComponent<ComponentType>(/* ... */);
/* ... */
}
World world;
};
template<typename Allocator>
class WorldI
{
template<typename ComponentType>
ComponentType* createComponent(/* ... */)
{
// do something with Allocator and ComponentType here
}
};
答案 1 :(得分:1)
请注意,这不是最佳解决方案(请参阅帖子底部的问题),但这是一种结合模板和虚拟功能的可行方法。我发布它希望你可以用它作为基础来提出更高效的东西。如果您无法找到改进方法,我建议将Entity
模板化为the other answer。
如果您不想对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中查看。
尽管如此,这有一些问题:
typeid
create_impl()
,失去了编译时类型扣除的好处。这导致执行速度比模板化时慢。
type_info
没有constexpr构造函数,甚至在typeid
参数为LiteralType
时也没有。ArgTuple<Ts...>
获取实际的Argument
类型,而不仅仅是投射和祈祷。任何这样做的方法都可能取决于RTTI,我想不出用它来映射type_index
es或类似于不同模板特化的任何方法。
assign()
呼叫站点隐式转换或转换参数,而不是让类型系统自动执行。这......有点问题。