C++ How to cache a variable of template type T in a class?

Viewed 416

Suppose that I have a Foo class like this, and I need many instances of it.

class Foo {
  public:
    Pool* bars;  // a global list of bars, each may have a different type

    template<typename T>
    T& AddBar(int x) {
        return bars->emplace<T>(x);
    }

    template<typename T>
    T& GetBar() {
        return bars->get<T>();  // a very slow function
    }
}

All the instances of Foo share the same pool of bars, which contains many bars of possibly different types. For example, bars may be a list of bars {A bar1, B bar2, A bar3, C bar4} where ABC are some class types, but each Foo foo instance can only have one bar of a specific type, e.g., a foo instance cannot have two bars of type A.

Given an instance Foo foo, I can get a specific type of bar using foo.GetBar<A>(), foo.GetBar<B>(), etc, but calling the bars->get<T>() function is slow and expensive. Therefore, I'm thinking of caching the result of GetBar() so that subsequent calls can return immediately without querying the pool again.

Now this is what I came up with: I created a static variable inside the member function to store the value of bar, it is only initialized and assigned value once.

template<typename T>
T& GetBar() {
    static T bar {};
    if (bar == T {}) {
        bar = bars->get<T>();  // a very slow function
    }
    return bar;
}

The problem is that, using the static keyword, this variable is now shared across all instances of Foo. If I try to get a bar of type A from different instances, they would return the same result.

Foo foo1;
Foo foo2;
foo1.AddBar<A>(1);
foo2.AddBar<A>(2);
foo1.GetBar<A>();  // returns a bar (type = A, value = 1)
foo2.GetBar<A>();  // returns the same bar with value 1, not 2

How can I cache every bar of type T inside the class and prevent it from being shared by other instances? I don't know how to store generic types as member variables, besides, storing each type T of bar can be a huge mess.

Edit: I know it'd be much easier to cache the result outside the class, on the caller's side. I'm just curious if there's an elegant way of caching inside the class.

Edit2: bars is a pointer to a registry pool, whose type is a complicated data structure, not a raw list or array. To clarify, I'm using the EnTT library to integrate entity-component-system into my application, but not sure how the pool is maintained internally in details.

Edit3: if you wonder what ABCs are, conceptually, these types are not known at compile time. but need to be determined at runtime. In fact, they are just many other class types I implemented, so I can also hardcode them into the Foo class, in which case I probably should use the factory pattern along with a scripting language for automatic code generation, but that would beat the purpose of using generics in the first place.

2 Answers

While writing a mockup, with the idea of n. 1.8e9-where's-my-share m., for your "complicated registry pool" I wrote the actual could be implementation of Foo. I left in there Foo only to also give some suggestions. If you want so have more than one variable of one type you would have to change the value type of the map of course, like from std::any to std::vector<std::any>. Otherwise please clarify your question more.

#include <iostream>
#include <string>
#include <map>
#include <any>

struct Pool {
    template<typename T>
    void emplace(T x) {
        this->elements_.insert_or_assign(typeid(T).hash_code(), std::make_any<T>(x));
    }

    template<typename T>
    T& get() {
        return std::any_cast<T&>(elements_.at(typeid(T).hash_code()));
    }

private:
    std::map<std::size_t, std::any> elements_;
};

class Foo {
public:
    Foo(Pool& pool): bars_(pool) {}

    void AddBar(int x) {
        return bars_.emplace<int>(x);
    }

    template<typename T>
    T& GetBar() {
        return bars_.get<T>();  // a very slow function
    }
private:
    Pool& bars_;
};

int main(){
    Pool pool;
    pool.emplace(4.3); pool.emplace(std::string("a value"));

    Foo foo1(pool);
    foo1.AddBar(3);

    std::cout << foo1.GetBar<int>() << "\n";
}

All ECS implementations abandon static type safety somewhere deep under their hood, although they can hide the ugly casts from the user or use the likes of std::any as in the other nice answer.

That said, here's an alternative way to approach it (simplified but it should give you the right idea) which avoids map lookups except once on calling the get function for a new type, T:

#include <iostream>
#include <unordered_map>
#include <typeinfo>
#include <any>

class Foo
{
public:
    template <class T>
    T& get()
    {
        // Fetch a unique index for T to use for our std::vector.
        const std::size_t n = type_index<T>();

        // Resize if it's a new type we're encountering.
        if (n >= bars.size())
            bars.resize(n+1);

        // Emplace if it's a former type of bar that's new for this instance
        // of Foo.
        if (!bars[n].has_value())
            bars[n].emplace<T>();

        // Returns the bar for that index cast to T&.
        return std::any_cast<T&>(bars[n]);
    }

private:
    // Stores all the elements.
    std::vector<std::any> bars;

    // Returns a unique type index for T.
    template <class T>
    static std::size_t type_index()
    {
        // Using static here avoids repeat lookups into the hash map.
        static const std::size_t n = lookup_type_index<T>();
        return n;
    }

    // Looks up a unique type index for T.
    template <class T>
    static std::size_t lookup_type_index()
    {
        // Warning: hash_code is not guaranteed to be unique for all
        // types in all compilers (two different types could return
        // the same hash code, e.g.). I recommend using something else but
        // that gets a bit involved (can expand and show you how if
        // needed). Also consider a lock here for thread safety.
        std::size_t key = typeid(T).hash_code();
        auto it = idxs.find(key);
        if (it != idxs.end())
            return it->second;
        idxs[key] = counter;
        return counter++;
    }
    static inline std::unordered_map<std::size_t, std::size_t> idxs;
    static inline std::size_t counter = 0;
};

int main()
{
    using namespace std;
    Foo f, f2;
    f.get<int>() = 123;
    f.get<double>() = 1.23;
    f2.get<int>() = 456;
    f2.get<double>() = 4.56;
    cout << f.get<int>() << endl;     // --> 123
    cout << f.get<double>() << endl;  // --> 1.23
    cout << f2.get<int>() << endl;    // --> 456
    cout << f2.get<double>() << endl; // --> 4.56
}

I didn't bother to test it but it should give you the gist of the idea. Update: I bothered to test it and updated it with a hasty program you can run after discovering some typos just double-checking what I wrote and realizing I ought to at least try to compile what I write. To avoid constantly looking into a type map, we map types to an index into a vector. You can extend that with sub-indices and so forth as your original example suggests. The above illustrates the main idea.

Please note the warning in the code above about std::type_info::hash_code as it applies to both mine and the other answer. I can offer a safe and portable alternative and one that doesn't even require RTTI but that gets a bit involved. You can usually find a bunch of examples out there if you search for ways to portably map a type T to an integer at compile-time that you can use at runtime.

Related