5

I'm aware of c++ templates, which allow you to write code for multiple types, but what if I want to store and access a type dynamically? Why is this so difficult to do in c++?

I would very much prefer to not have to do something like this:

enum SupportedTypes
{
    IntType,
    FloatType,
    StringType
}

template <typename T>
class ClassThing
{
    public:
        T Value;
        SupportedTypes Type;
}

...

//Not sure if you could even access thing->Type, but regardless, you get the idea...
switch (thing->Type)
{
    case IntType:
        DoSomething(((ClassThing<int>*)thing)->T);
        break;
    case FloatType:
        DoSomething(((ClassThing<float>*)thing)->T);
        break;
    case StringType:
        DoSomething(((ClassThing<string>*)thing)->T);
        break;
}

Why doesn't c++ support something like this:

int whatIsThis = 5;
type t = typeid(whatIsThis); //typeid exists, but you can't do...:
t anotherInt = 5;

?

Another question that I have that I'm more optimistic of receiving a good answer to: if you choose to go the templated route, is there any way to maintain the type if you store it generically in a collection? E.g.:

vector<ClassThing> things;

(This will give an "argument list for class template ... is missing" error, by the way.) My guess is that no, this is not possible because the above is not possible.

GEOCHET
  • 21,119
  • 15
  • 74
  • 98
Andrew
  • 5,839
  • 1
  • 51
  • 72

5 Answers5

9

How do I store and access a type dynamically in c++?

There are many options to pick from:

  • use runtime polymorphism, where you have a base class that might offer some common functionality and derived classes for each supported type; you often have to make some choices about how "fat" your interface should be (providing base class functions that only work meaningfully for a subset of derived types) vs. forcing the client to use dynamic_cast<> to recover/switch-on the runtime type

    • a particularly powerful technique is having the derived classes be type-specific instantiations of the same template, as it means you can support arbitrary types parametrically, i.e. if they provide the semantics of usage that the template expects
  • use a discriminated union (basically, a type identification enum/int alongside a union of the supported types) - std::variant<> is a good choice for this

  • when creating/storing a value capture you'll necessarily know it's type

    • you can record both its typeinfo and address, then when accessing the variable later you can use the typeinfo to test whether the object is of a specific type - trying each supported type until a match is found - std::any<> is a good choice for this, or

    • you can capture an arbitrary set of type-specific operations using function pointers or std::function<>

Why doesn't c++ support something like this:

int whatIsThis = 5;
type t = typeid(whatIsThis); //typeid exists, but you can't do...:
t anotherInt = 5;?

It does, with decltype and auto:

int whatIsThis = 5;
using t = decltype(whatIsThis);
t anotherInt = 5;

auto anotherWhatever = whatIsThis; // another way to create an additional
                                   // variable of the same type

For runtime polymorphism, you might actually want to read up on factories (which create one of many types of object - all derived from some base interface - given some runtime input), and clone functions (which create a copy of a variable of unknown runtime type).

if you choose to go the templated route, is there any way to maintain the type if you store it generically in a collection: vector<ClassThing> things; (This will give an "argument list for class template ... is missing" error, by the way.)

You can't create even a single object from a template without instantiating it, so no there's no way to have an entire vector either. A reasonable approach is to derive the template from a base class and store [smart] pointers or std::reference_wrappers to the base class in the vector.

Tony Delroy
  • 102,968
  • 15
  • 177
  • 252
3
int x = 5;
decltype(x) y = 4;
auto z = 3;

decltype(a) will give you the type of a. You can then use typedef to store the types, or other functions to remove references from the type if necessary.

For example:

typedef decltype(a) type1; 
type1 b = 2 * a;

auto makes you not need to specify the type at all.

The only thing you need is to compile in c++11 mode (-std=c++11) or later.

As for the vector question, decltype will work there too.

coyotte508
  • 9,175
  • 6
  • 44
  • 63
2

I won't steal the answer, but I will provide the method I ended up using for those who are trying to do something similar. (I am writing my own raw serialization and deserialization code with memcpy.) What I had hoped to do was store and maintain various arrangements of types without having to create a bunch of structs or classes, e.g. (from my question):

template <typename T>
class ClassThing
{
    public:
        T Value;
        SupportedTypes Type;
}

//Then store everything in a:
vector<ClassThing> things;

However, attempting to store a templated class in a vector will give an "argument list for class template ... is missing" error, because as Tony D said in his answer, "You can't create even a single object from a template without instantiating it..." I also didn't want to use any external libraries like boost (for variants).

So, I concluded that because I absolutely wanted to use a single collection to store all of the structures, I simply could not use a templated class. Instead, I resolved to use a templated constructor (only) and a void* for the Value, as well as store the type's hash and the number of bytes required for storing/copying the type:

class ClassThing
{
    public:
        void* Value;

        unsigned long long TypeHash;

        unsigned long long NumberOfBytes;

        template <typename T>
        ClassThing(T passedValue)
        {
            Value = &passedValue;
            TypeHash = typeid(passedValue).hash_code();
            NumberOfBytes = sizeof(T);
        }

        //For strings, do this:
        ClassThing(const char* passedValue, unsigned short passedNumberOfBytes)
        {
            Value = const_cast<char*>(passedValue);
            TypeHash = typeid(char*).hash_code();
            NumberOfBytes = length;
        }
}

Unfortunately, this solution loses the type, but since the serialization and deserialization process I'm using is a simple memcpy, all I needed was a pointer to the data and the number of bytes it used. The reason I store the type's hash here is so that I can perform type checking before serialization (e.g. make sure a float isn't being serialized where an int should be).

For the deserialization process, I will be using this technique: https://stackoverflow.com/a/15313677/1599699

Since I do not know the type, I will simply have to expect that the cast from void* matches up with the serialization process, although I can at least check the NumberOfBytes value and ideally the TypeHash as well, if those are available. On the deserialization end, I will end up with a void* and do this:

void* deserializedData = ...;
float deserializedFloat = *(float*)&deserializedData;

This of course is not the ideal solution to my problem, but it allows me to do what I want, which is extremely high performance serialization and deserialization to binary with low memory usage and extremely low maintenance.

Hope this helps someone!

Community
  • 1
  • 1
Andrew
  • 5,839
  • 1
  • 51
  • 72
  • Regarding “… without having to create a bunch of structs or classes”. Why? I think a lot of beginner/intermediate C++ programmers think defining a struct is a Big Deal for some reason. I regularly use one-off structs as return types or argument types to just one function. Need a struct you don’t have? Define it! – Ben Oct 20 '21 at 02:32
  • @Ben I'm not nor was I beginner/intermediate. You have it backwards. Lots of structs and classes make your code rigid and difficult to change later, especially when there's lots of code tied to them. – Andrew Oct 21 '21 at 06:17
  • That’s an interesting perspective. My experience is that having few monolithic classes makes code difficult to change. – Ben Oct 21 '21 at 10:32
1

Although this is not exactly a C++ answer (rather, a C one), it should be valid in C++ all the same.

The type void* is a pointer to untyped memory. Basically, you can cast it to any type of pointer, then dereference. Example:

int x1 = 42;
long l1 = 123456789L;

void* test = &x1;
int x2 = *(int*)test; // x2 now contains the contents of x1

test = &l1;
long l2 = *(long*)test; // l2 now contains the contents of l1

This is in no way the most delicate way of solving your problem, but it is an option.

Further reading:
https://www.astro.umd.edu/~dcr/Courses/ASTR615/intro_C/node15.html
http://www.circuitstoday.com/void-pointers-in-c
http://www.nongnu.org/c-prog-book/online/x658.html

Levi
  • 1,921
  • 1
  • 14
  • 18
  • 1
    I'm aware of void*, but this would require me implementing my own type conversions manually (i.e. switch statement, like in my question), no? The problem was not in how to store types but in how to retrieve them afterwards. – Andrew May 11 '15 at 14:55
0

If you want dynamic types (in C++11 or better, e.g. C++14) you could make a variant type by making a class with some union:

 class Thing {
   enum SupportedTypes type;
   union {
     intptr_t num; // when type == IntType
     double flo; // when type == FloatType
     std::string str; // when type == StringType
   }
   // etc....
 };

Be careful, you need to obey to the rule of five and you probably should explicitly call the destructor of std::string on str when type == StringType, etc...

Some third party libraries might be helpful: Boost variants, Qt QVariant, etc...

Basile Starynkevitch
  • 223,805
  • 18
  • 296
  • 547
  • 1
    Given the lack of awareness of C++ features/issues displayed by the OP, it's more practical to suggest `boost::variant<>` than an attempt to re-create the same kind of functionality. – Tony Delroy May 11 '15 at 05:14
  • But Boost is not standard – Basile Starynkevitch May 11 '15 at 05:15
  • 1
    Maybe I don't want to use Boost. Vanilla c++ variants like this do not solve my problem, as they would require usage of a switch or etc. (no dynamic runtime type retrieval), so the only answer you're really providing here is to use these libraries. – Andrew May 11 '15 at 16:36