解决c++中缺乏模板化虚函数的问题

Getting around the lack of templated virtual functions in C++

本文关键字:函数 问题 c++ 解决      更新时间:2023-10-16

我不确定如何最好地表达这个问题,但我不是在问如何实现模板化虚函数本身。我正在构建一个实体组件系统,我有两个重要的类——WorldEntity。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, WorldWorldImpl

// 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()调用站点隐式转换或强制类型转换参数,而不是让类型系统自动执行。这个…是一个小问题。