简体   繁体   English

在C ++中解决缺少模板化虚函数的问题

[英]Getting around the lack of templated virtual functions in C++

I'm not sure how best to phrase the question, but I'm not asking how to implement templated virtual functions per-se. 我不确定如何最好地表达这个问题,但我不是问如何实现模板化的虚函数本身。 I'm building an entity component system, and I have two important classes - World and Entity . 我正在构建一个实体组件系统,我有两个重要的类 - WorldEntity World is actually an abstract class, and the implementation (let's call it WorldImpl ) is a templated class that allows use of a custom allocator (one that can be used with std::allocator_traits ). World实际上是一个抽象类,实现(我们称之为WorldImpl )是一个模板化的类,它允许使用自定义分配器(可以与std::allocator_traits一起使用)。

Components are any data type which we can attach to entities. 组件是我们可以附加到实体的任何数据类型。 This is done by calling a templated function named assign on the entity. 这是通过在实体上调用名为assign的模板化函数来完成的。

Here's the problem: I'm trying to make the entity use the world's allocator when creating and initializing components. 这就是问题所在:我在创建和初始化组件时试图让实体使用世界的分配器。 In a perfect world, you would call Entity::assign<ComponentType>( ... ) which would ask the WorldImpl to create the component with whatever allocator is appropriate. 在一个完美的世界中,你会调用Entity::assign<ComponentType>( ... ) ,它会要求WorldImpl用任何适当的分配器创建组件。 There's a problem here, however - The entity has a pointer to World and templated virtual functions aren't possible to my knowledge. 但是这里有一个问题 - 实体有一个指向World的指针,根据我的知识,模板化的虚函数是不可能的。

Here's a bit more of an illustration that might make the issue more obvious: 以下是一个可能使问题更加明显的插图:

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
  }
};

Seeing as the above code isn't actually possible, here's the real question: With a class hierarchy such as this, what black magic do I have to do in order for some function to be called with both the ComponentType and Allocator template parameters? 看上面的代码实际上是不可能的,这是真正的问题:对于像这样的类层次结构,为了使用ComponentType和Allocator模板参数调用某些函数,我还需要做些什么黑魔法? This is the ultimate goal - a function called on some object with both template parameters available to it. 这是最终目标 - 一个函数调用某个对象,并且可以使用两个模板参数。

I'd say that Entities belong to a certain kind of world and make them templates with a World parameter. 我会说实体属于某种世界,并使用World参数制作模板。 Then you can forget about all the inheritance and virtual and just implement worlds that fulfill the required interface, eg 然后你可以忘记所有的继承和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
  }
};

Note that this isn't an optimal solution (see the bottom of the post for issues), but a somewhat-viable way to combine templates and virtual functions. 请注意,这不是最佳解决方案(请参阅帖子底部的问题),但这是一种结合模板和虚拟功能的可行方法。 I post it in the hopes that you can use it as a basis to come up with something more efficient. 我发布它希望你可以用它作为基础来提出更高效的东西。 If you can't find a way to improve on this, I would suggest templating Entity , as the other answer suggested. 如果你找不到改进的方法,我会建议模板Entity ,正如另一个答案所暗示的那样。


If you don't want to do any major modifications to Entity , you can implement a hidden virtual helper function in World , to actually create the component. 如果您不想对Entity进行任何重大修改,可以在World实现隐藏的虚拟助手函数,以实际创建组件。 In this case, the helper function can take a parameter which indicates what kind of component to construct, and return void* ; 在这种情况下,辅助函数可以采用一个参数来指示要构造的组件类型,并返回void* ; createComponent() calls the hidden function, specifying ComponentType , and casts the return value to ComponentType* . createComponent()调用隐藏函数,指定ComponentType ,并将返回值强制转换为ComponentType* The easiest way I can think of is to give each component a static member function, create() , and map type indexes to create() calls. 我能想到的最简单的方法是为每个组件提供一个静态成员函数, create()和map类型索引来create()调用。

To allow each component to take different parameters, we can use a helper type, let's call it Arguments . 为了允许每个组件采用不同的参数,我们可以使用帮助器类型,让我们称之为Arguments This type provides a simple interface while wrapping the actual parameter list, allowing us to easily define our create() functions. 这种类型在包装实际参数列表时提供了一个简单的接口,允许我们轻松定义我们的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;
};

Next, we define our components to have a create() function, which takes a const Arguments& and grabs arguments out of it, by calling get() , dereferencing the pointer, casting the pointed-to ArgTuple<Ts...> to match the component's constructor parameter list, and finally obtaining the actual argument tuple with get() . 接下来,我们定义我们的组件有一个create()函数,它接受const Arguments&并从中获取const Arguments& ,通过调用get() ,解除引用指针,转换指向的ArgTuple<Ts...>来匹配组件的构造函数参数列表,最后用get()获取实际的参数元组。

Note that this will fail if the Arguments was constructed with an improper argument list (one that doesn't match the component's constructor's parameter list), just as calling the constructor directly with an improper argument list would; 请注意,如果使用不正确的参数列表(一个与组件的构造函数的参数列表不匹配的参数列表)构造Arguments ,这将失败,就像使用不正确的参数列表直接调用构造函数一样; it will accept an empty argument list, however, due to Arguments::operator bool() , allowing default parameters to be provided. 但是,由于Arguments::operator bool()它将接受一个空参数列表,允许提供默认参数。 [Unfortunately, at the moment, this code has issues with type conversion, specifically when the types aren't the same size. [不幸的是,目前,此代码存在类型转换问题,特别是当类型大小不同时。 I'm not yet sure how to fix this.] 我还不确定如何解决这个问题。]

// 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);

Then, with all that in place, we can finally implement Entity , World , and WorldImpl . 然后,有了所有这些,我们终于可以实现EntityWorldWorldImpl

// 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.
}

See it in action here . 这里看到它。


That said, this has a few issues: 也就是说,这有一些问题:

  • Relies on typeid for create_impl() , losing the benefits of compile-time type deduction. 依赖于create_impl() typeid ,失去了编译时类型推导的好处。 This results in slower execution than if it was templated. 这导致执行速度比模板化时慢。
    • Compounding the issue, type_info has no constexpr constructor, not even for when the typeid parameter is a LiteralType . 使问题更复杂的是, type_info没有constexpr构造函数,甚至在typeid参数是LiteralType时也没有。
  • I'm not sure how to obtain the actual ArgTuple<Ts...> type from Argument , rather than just casting-and-praying. 我不知道如何从Argument获得实际的ArgTuple<Ts...>类型,而不仅仅是投射和祈祷。 Any methods of doing so would likely depend on RTTI, and I can't think of a way to use it to map type_index es or anything similar to different template specialisations. 任何这样做的方法都可能取决于RTTI,我想不出用它来映射type_index es或类似于不同模板特化的任何方法。
    • Due to this, arguments must be implicitly converted or casted at the assign() call site, instead of letting the type system do it automatically. 因此,必须在assign()调用站点隐式转换或转换参数,而不是让类型系统自动执行。 This... is a bit of an issue. 这......有点问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM