Search code examples
c++templatesmemberstatic-polymorphismgenerative-programming

Templated member variables in C++


Often when writing templated code, I find myself needing to store an instance of the template type in a member variable. For example, I might need to cache a value to be used later on. I would like to be able to write my code as:

struct Foo
{
    template<typename T>
    T member;
    
    template<typename T>
    void setMember(T value)
    {
        member<T> = value;
    }

    template<typename T>
    T getMember()
    {
        return member<T>;
    }
};

Where members are specialized as they are used. My question:

  1. Is such templated member variable possible with current C++ generative coding facilities?
  2. If not, are there any proposals for such a language feature?
  3. If not, are there any technical reasons why such a thing is not possible?

It should be obvious that I do not want to list all possible types (e.g. in a std::variant) as that is not generative programming and would not be possible if the user of the library is not the same as the author.

Edit: I think this somewhat answers my 3rd question from above. The reason being that today's compilers are not able to postpone instantiation of objects to after the whole program has been parsed: https://stackoverflow.com/a/27709454/3847255


Solution

  • This is possible in the library by combining existing facilities.

    The simplest implementation would be

    std::unordered_map<std::type_index, std::any>
    

    This is mildly inefficient since it stores each std::type_index object twice (once in the key and once inside each std::any), so a std::unordered_set<std::any> with custom transparent hash and comparator would be more efficient; this would be more work though.

    Example.

    As you say, the user of the library may not be the same as the author; in particular, the destructor of Foo does not know which types were set, but it must locate those objects and call their destructors, noting that the set of types used may be different between instances of Foo, so this information must be stored in a runtime container within Foo.

    If you're wary about the RTTI overhead implied by std::type_index and std::any, we can replace them with lower-level equivalents. For std::type_index you can use a pointer to a static tag variable template instantiation (or any similar facility), and for std::any you can use a type-erased std::unique_ptr<void, void(*)(void*)> where the deleter is a function pointer:

    using ErasedPtr = std::unique_ptr<void, void(*)(void*)>;
    std::unordered_map<void*, ErasedPtr> member;
    struct tag {};
    template<class T> inline static tag type_tag;
    
        member.insert_or_assign(&type_tag<T>, ErasedPtr{new T(value), [](void* p) {
            delete static_cast<T*>(p);
        }});
    

    Example. Note that once you make the deleter of std::unique_ptr a function pointer, it is no longer default-constructible, so we can't use operator[] any more but must use insert_or_assign and find. (Again, we've got the same DRY violation / inefficiency, since the deleter could be used as the key into the map; exploiting this is left as an exercise for the reader.)