0

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

pooya13
  • 2,060
  • 2
  • 23
  • 29
  • It is not possible, because after you've processed that header, what is `sizeof(Foo)`? A class must know it's own size after it's fully defined, and this isn't possible with template members. – Mooing Duck Feb 19 '21 at 19:00
  • You can use `std::unordered_map` to kind of fake it, but in general, it's best to rethink your design. – Mooing Duck Feb 19 '21 at 19:01
  • @MooingDuck My point is that these member-templated types and every dependent type can be instantiated after all the program has been parsed at link time. Obviously that is not how current linkers work but it is not impossible either. That is why I asked the 3rd part of my question. Or do you think this is logically impossible to achieve? – pooya13 Feb 19 '21 at 19:07
  • Yes, it is logically impossible, because in order to fully define the `Foo` type to know how big it is, it must first parse and link the entire application. But in order to parse and link the application, the compiler must first know how big a `Foo` is. – Mooing Duck Feb 19 '21 at 19:53

2 Answers2

1

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

ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • 2
    A type index is a pointer under the hood. Not very expensive. – Yakk - Adam Nevraumont Jan 16 '21 at 00:02
  • @Yakk-AdamNevraumont yes, so "mildly" inefficient. It does violate DRY to store it twice, though, and raises the risk of the two copies of the pointer to `std::typeinfo` somehow getting out of sync, so that's an invariant that must be maintained. – ecatmur Jan 16 '21 at 00:04
  • Thank you @ecatmur for your answer. Could you elaborate on the sync issue? I like your answer best so far since it answers my question. However I would like to avoid runtime over-head and RTTI if I don't need them (since in my question, all my types are known at compile time) – pooya13 Jan 16 '21 at 07:13
  • 1
    @pooya13 the types are known at compile time, but they aren't known in the same place. The compiler/linker lets us get `static` template variables per each type, but to associate them with an instance of `Foo` we need a runtime container. – ecatmur Jan 16 '21 at 12:18
  • @ecatmur Why does the fact that the types are not all known at the same place matter? Can't the compiler say I need to look at the whole program before I know this type fully? – pooya13 Jan 16 '21 at 19:14
  • 2
    If you have multiple source files then they get combined into a program by the linker; all the linker knows how to do is to identify template instantiations (e.g. of variable templates, or of class templates and their static members, or of function templates and their static local variables). Certainly it's possible to conceive of a language that has what you want to do as a language feature; but that language isn't current C++. – ecatmur Jan 16 '21 at 22:10
  • @pooya13: `dll` files are a way to add more types _after the process is already installed_, so no. The compiler can't look at the whole program to know the type fully. – Mooing Duck Feb 19 '21 at 19:55
1

Is such templated member variable possible with current C++ generative coding facilities?

No, not exactly what you describe. What is possible is to make the enclosing class a template and use the template parameters to describe the types of the class' members.

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

In C++14 and later, there are variable templates, but you can't make a template non-static data member of a class.

If not, are there any proposals for such a language feature?

Not that I know of.

If not, are there any technical reasons why such a thing is not possible?

The primary reason is that that would make it impossible to define binary representation of the class. As opposed to templates, a class is a type, which means its representation must be fixed, meaning that at any place in the program Foo and Foo::member must mean the same things - same types, same object sizes and binary layout, and so on. A template, on the other hand, is not a type (or, in case of variable templates, is not an object). It becomes one when it is instantiated, and each template instantiation is a separate type (in case of variable templates - object).

Andrey Semashev
  • 10,046
  • 1
  • 17
  • 27
  • "The primary reason is that that would make it impossible to define binary representation of the class." How so? Wouldn't the binary representation be fully defined after the whole program has been parsed and it is known what member types have been specialized? – pooya13 Jan 16 '21 at 07:42
  • Thank you for your answer but how does your template class in any way answer my question? I would not be able to instantiate multiple members of different types in the same class. – pooya13 Jan 16 '21 at 07:43
  • @pooya13 > Wouldn't the binary representation be fully defined after the whole program has been parsed and it is known what member types have been specialized? -- The point is that `Foo` must have the fixed single representation, meaning that `Foo::member` must have a fixed single type at any point in the program. This would not be the case if `Foo::member` was a template and could be instantiated with different types in different parts of the program. – Andrey Semashev Jan 16 '21 at 08:04
  • > I would not be able to instantiate multiple members of different types in the same class. -- You can have multiple template parameters - one for each member. – Andrey Semashev Jan 16 '21 at 08:06
  • > You can have multiple template parameters - one for each member. -- That means I would only have a certain number of member variables. That is not what I am asking. I want to have generative code where member variables are specialized as they are used. – pooya13 Jan 16 '21 at 19:11
  • > The point is that Foo must have the fixed single representation, meaning that Foo::member must have a fixed single type at any point in the program -- I don't see how this is any different from not being able to use `Foo::function`. You would have to do `Foo::member` the same way you would do `Foo::function` when you need to use a template member function. What am I missing? – pooya13 Jan 16 '21 at 19:13
  • @pooya13 > I want to have generative code where member variables are specialized as they are used. -- This is not possible with current C++. – Andrey Semashev Jan 16 '21 at 19:21
  • > I don't see how this is any different from not being able to use `Foo::function`. -- Member functions don't contribute to the class' representation. (NB: virtual functions do, but they cannot be templates - exactly because of this.) When you instantiate a member function template, a new function is created; the class representation stays the same. – Andrey Semashev Jan 16 '21 at 19:27
  • You have to understand that a type must be defined in a single way at compile time in C++. So a type `Foo` must have a single binary layout. A template allows to generate types, so e.g. `Foo` and `Foo` are different types and may have different `member` member variables. What you're asking makes `Foo` definition ambiguous. – Andrey Semashev Jan 16 '21 at 19:35
  • If you want to generate data structures based on types, at compile time, look at how `std::pair`, `std::tuple` and Boost.Fusion work. Otherwise you will have to implement something that works in run time - `std::variant`, `std::any` and containers may help you here. – Andrey Semashev Jan 16 '21 at 19:37
  • > You have to understand that a type must be defined in a single way at compile time in C++. -- Wouldn't the compiler know the type at compile time after parsing the whole program? Why can't it keep note of all members having been specialized so far, and when program is fully parsed, instantiate the type where ever needed? – pooya13 Jan 17 '21 at 03:10
  • @pooya13 First, the compiler cannot parse the whole program, in general. Second, `Foo` is not a template in your proposal, it cannot be instantiated. – Andrey Semashev Jan 17 '21 at 08:46
  • What about whole program optimization then? What do you mean it cannot be instantiated? – pooya13 Jan 17 '21 at 09:13
  • @pooya13 > What about whole program optimization then? -- WPO is an optional optimization, which is not universally supported or enabled. And it doesn't take into account the code in libraries, which are already fully compiled. – Andrey Semashev Jan 17 '21 at 11:25
  • > What do you mean it cannot be instantiated? -- Instantiation is a process of generation of a type (or a function or an object) from a template of a type (or a function or a variable). You can't "instantiate" a type as there is nothing to instantiate - the type is already complete. In other words, you can build a building from a blueprint, but you can't build a building from a building - the building is already there. Similarly, you can't have a blueprint instead of a wall in the building. The wall must be there or not there. – Andrey Semashev Jan 17 '21 at 11:29
  • I think you have **Instantiation** and **Specialization** confused? When you create an object of a type it is referred to as instantiating the type. Thank you for your explanation of WPO. – pooya13 Jan 18 '21 at 04:22
  • @pooya13 The process of creation of an object (which indeed can be called instantiation) is not relevant to my answer or the following discussion. I used the term exclusively wrt. templates and types. Specialization is a different process, which is also related to templates and allows to change the result of template instantiation depending on template arguments beyond just substitution. I'm not touching that subject in my answer. – Andrey Semashev Jan 18 '21 at 13:04