简体   繁体   中英

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 . 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 ).

Components are any data type which we can attach to entities. This is done by calling a templated function named assign on the entity.

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. There's a problem here, however - The entity has a pointer to World and templated virtual functions aren't possible to my knowledge.

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? 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. Then you can forget about all the inheritance and virtual and just implement worlds that fulfill the required interface, eg

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.


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. In this case, the helper function can take a parameter which indicates what kind of component to construct, and return void* ; createComponent() calls the hidden function, specifying ComponentType , and casts the return value to 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.

To allow each component to take different parameters, we can use a helper type, let's call it Arguments . This type provides a simple interface while wrapping the actual parameter list, allowing us to easily define our create() functions.

// 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() .

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; it will accept an empty argument list, however, due to Arguments::operator bool() , allowing default parameters to be provided. [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 .

// 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. 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 .
  • I'm not sure how to obtain the actual ArgTuple<Ts...> type from Argument , rather than just casting-and-praying. 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.
    • Due to this, arguments must be implicitly converted or casted at the assign() call site, instead of letting the type system do it automatically. This... is a bit of an issue.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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