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

时间:2016-10-27 15:22:33

标签: c++ c++11 templates

我不确定如何最好地表达这个问题,但我不是问如何实现模板化的虚函数本身。我正在构建一个实体组件系统,我有两个重要的类 - 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模板调用某些函数参数?这是最终目标 - 一个函数调用某个对象,并且可以使用两个模板参数。

2 个答案:

答案 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);

然后,有了所有这些,我们终于可以实施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.
}

在行动here中查看。

尽管如此,这有一些问题:

  • 依赖于typeid create_impl(),失去了编译时类型扣除的好处。这导致执行速度比模板化时慢。
    • 如果问题更加复杂,type_info没有constexpr构造函数,甚至在typeid参数为LiteralType时也没有。
  • 我不确定如何从ArgTuple<Ts...>获取实际的Argument类型,而不仅仅是投射和祈祷。任何这样做的方法都可能取决于RTTI,我想不出用它来映射type_index es或类似于不同模板特化的任何方法。
    • 因此,必须在assign()呼叫站点隐式转换或转换参数,而不是让类型系统自动执行。这......有点问题。