解决c++中缺乏模板化虚函数的问题
Getting around the lack of templated virtual functions in C++
我不确定如何最好地表达这个问题,但我不是在问如何实现模板化虚函数本身。我正在构建一个实体组件系统,我有两个重要的类——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模板参数?这是最终的目标——在一个对象上调用一个函数,其中两个模板参数都可用。
我想说实体属于某种世界,并使它们具有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
}
};
注意,这不是一个最佳的解决方案(见文章底部的问题),但某种程度上可行的方式来结合模板和虚拟函数。我把它贴出来,希望你能以此为基础,想出更有效的办法。如果你不能找到一种方法来改进这一点,我建议模板化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()
函数,该函数通过调用get()
,解引用指针,强制转换指向的ArgTuple<Ts...>
以匹配组件的构造函数参数列表,最后使用get()
获得实际的参数元组,从而获取const Arguments&
并从中获取参数。
注意,如果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);
}
}
// Testing function.
friend std::ostream& operator<<(std::ostream& os, const Two& two)
{
return os << "Two, with "
<< (two.c == ' ' ? "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.
}
在这里看到它的作用
也就是说,这有几个问题:
- 对
create_impl()
依赖typeid
,失去了编译时类型扣除的好处。这将导致执行速度比模板化慢。- 更复杂的是,
type_info
没有constexpr构造函数,即使typeid
参数是LiteralType
也没有。
Argument
中获得实际的ArgTuple<Ts...>
类型,而不仅仅是铸造和祈祷。这样做的任何方法都可能依赖于RTTI,我想不出一种方法可以使用它来映射type_index
es或类似于不同模板专门化的任何东西。- 因此,必须在
assign()
调用站点隐式转换或强制类型转换参数,而不是让类型系统自动执行。这个…是一个小问题。
- 类C++中的函数问题(LNK2019和LNK1120错误)
- 返回不停止函数,递归函数问题?(编程练习,动态规划,Levenshtein 回溯)
- 变分模板递归构造函数问题
- C++函数问题中的数组分配
- C++中嵌套在另一个 LinkedList 中的 LinkedList 整数上的回调函数问题
- C++重载构造函数问题
- 二叉搜索树析构函数问题
- C++ vector<pair<int,int>> std::all_of() 函数问题(重定向到头文件)
- 迷宫构造函数问题 [线程 1:EXC_BAD_ACCESS(代码 = 1,地址 = 0x8)]
- C++函数问题中的 IF 语句
- 函数问题,未定义的引用错误
- C++ 指针无效函数问题
- C++函数问题 = 帮助我理解基本概念
- 由于基于接口的编程,遇到 C++ 虚拟模板函数问题
- C++二进制树递归析构函数问题
- 构造函数问题<无法读取内存>
- 构造函数问题
- C 时间函数问题
- 另一个类中的类的构造函数问题
- C与C++函数问题