【问题标题】:Getting around the lack of templated virtual functions in C++解决 C++ 中缺乏模板化虚函数的问题
【发布时间】:2016-10-27 15:22:33
【问题描述】:

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

【问题讨论】:

  • CRTP,也许?
  • 呵呵,我以前没见过这种模式。这可能行得通,我必须做一些工作来解决这个问题。感谢您的提示!
  • 几个问题:我猜实体将拥有或至少拥有指向组件的指针?这是否意味着组件也有一个基类?因为实体没有模板化?如果不是实体,谁将拥有组件?
  • 分配与施工分开。 allocate 可以是虚拟非模板函数,createComponent 可以是非虚拟函数模板。
  • @SamBloomberg 可以忽略分配器特征的构造/销毁方法,只使用放置新/手动销毁。使用分配最大对齐类型(通常为双精度)的分配器,然后将大小传递给它。除了放置 new 之外,分配器很少使用构造来做一些事情,而且 STL 分配器一开始就这样做可能很糟糕。

标签: c++ c++11 templates


【解决方案1】:

我会说实体属于某种世界,并使用 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
  }
};

【讨论】:

  • 不幸的是,这似乎是我必须这样做的方式。我想让模板远离实体,但我没有看到任何其他选项。
【解决方案2】:

请注意,这不是最佳解决方案(有关问题,请参阅文章底部),而是一种结合模板和虚函数的可行方法。我发布它是希望您可以以此为基础提出更有效的方法。如果您找不到改进的方法,我建议按照the other answer 的建议模板化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() 函数,该函数接受const Arguments&amp; 并从中获取参数,方法是调用get(),取消引用指针,将指向的ArgTuple&lt;Ts...&gt; 转换为匹配组件的构造函数参数列表,最后得到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


也就是说,这有几个问题:

  • create_impl() 依赖于typeid,失去了编译时类型推导的好处。与模板化相比,这会导致执行速度变慢。
    • 使问题更加复杂的是,type_info 没有 constexpr 构造函数,即使 typeid 参数是 LiteralType 时也没有。
  • 我不确定如何从Argument 获取实际的ArgTuple&lt;Ts...&gt; 类型,而不仅仅是铸造和祈祷。这样做的任何方法都可能取决于 RTTI,我想不出一种方法来使用它来映射 type_indexes 或任何类似于不同模板专业化的东西。
    • 因此,参数必须在assign() 调用点进行隐式转换或强制转换,而不是让类型系统自动完成。这……有点问题。

【讨论】:

  • 是的,虽然不是最佳选择,但仍然是一个不错的选择。谢谢!
  • @SamBloomberg 不客气。希望它会派上用场。
猜你喜欢
  • 1970-01-01
  • 2014-10-08
  • 2017-07-18
  • 1970-01-01
  • 2020-02-17
  • 2012-11-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多