C++ std::map holding ANY type of value
Asked Answered
F

8

55

Basically I want MyClass that holds a Hashmap that maps Field name(string) to ANY type of Value.. For this purpose I wrote a separate MyField class that holds the type & value information..

This is what I have so far:

template <typename T>
class MyField {
    T m_Value;
    int m_Size;
}


struct MyClass {
    std::map<string, MyField> fields;   //ERROR!!!
}

But as you can see, the map declaration fails because I didn't provide the type parameter for MyField...

So I guess It has to be something like

std::map< string, MyField<int> > fields;

or

std::map< string, MyField<double> > fields;


But obviously this undermines my whole purpose, because the declared map can only hold MyField of a specific type.. I want a map that can hold ANY type of MyField clas..

Is there any way I can achieve this..?

Foti answered 11/7, 2014 at 16:18 Comment(6)
You need some kind of type erasure. I recommend boost::any.Brilliantine
You could use std::map<std::string, std::shared_ptr<void>>.Impermissible
@sharth Is there any reason why you're using shared_ptr<void> instead of simply (void *)?Foti
I think that's taking type erasure too far, using void * (shared_ptr<void> is the same). You'd need at least an extra value to figure out what that thing is. I'd either use a pointer to a base class or boost::variant<> if that's unavailable, unless you're writing really low level code.Griffey
If it's really just any MyField<T>, I guess another option is a base class that each MyField<T> inherits from.Brilliantine
@user3794186: For the purposes of achieving your goal, there's no significant difference. But it does make some memory management details easier.Impermissible
M
42

Blindy's answer is very good (+1), but just to complete the answer: there is another way to do it with no library, by using dynamic inheritance:

class MyFieldInterface
{
    int m_Size; // of course use appropriate access level in the real code...
    ~MyFieldInterface() = default;
}

template <typename T>
class MyField : public MyFieldInterface {
    T m_Value; 
}


struct MyClass {
    std::map<string, MyFieldInterface* > fields;  
}

Pros:

  • it's familiar to any C++ coder
  • it don't force you to use Boost (in some contexts you are not allowed to);

Cons:

  • you have to allocate the objects on the heap/free store and use reference semantic instead of value semantic to manipulate them;
  • public inheritance exposed that way might lead to over-use of dynamic inheritance and a lot of long-term issues related to your types really being too inter-dependent;
  • a vector of pointers is problematic if it have to own the objects, as you have to manage destruction;

So use boost::any or boost::variant as default if you can, and consider this option only otherwise.

To fix that last cons point you could use smart pointers:

struct MyClass {
    std::map<string, std::unique_ptr<MyFieldInterface> > fields;  // or shared_ptr<> if you are sharing ownership
}

However there is still a potentially more problematic point:

It forces you to create the objects using new/delete (or make_unique/shared). This mean that the actual objects are created in the free store (the heap) at any location provided by the allocator (mostly the default one). Therefore, going though the list of objects very often is not as fast as it could be because of cache misses.

diagram of vector of polymorphic objects

If you are concerned with performance of looping through this list very often as fast as possible (ignore the following if not), then you'd better use either boost::variant (if you already know all the concrete types you will use) OR use some kind of type-erased polymorphic container.

diagram of polymorphic container

The idea is that the container would manage arrays of objects of the same type, but that still expose the same interface. That interface can be either a concept (using duck-typing techniques) or a dynamic interface (a base class like in my first example). The advantage is that the container will keep same-type objects in separate vectors, so going through them is fast. Only going from one type to another is not.

Here is an example (the images are from there): http://bannalia.blogspot.fr/2014/05/fast-polymorphic-collections.html

However, this technique loose it's interest if you need to keep the order in which the objects are inserted.

In any way, there are several solutions possible, which depends a lot on your needs. If you have not enough experience with your case, I suggest using either the simple solution I first explained in my example or boost::any/variant.


As a complement to this answer, I want to point very good blog articles which summarize all C++ type-erasure techniques you could use, with comments and pros/cons:

Mylo answered 11/7, 2014 at 16:39 Comment(0)
T
77

This is plain in C++ 17. Use std::map + std::any + std::any_cast:

#include <map>
#include <string>
#include <any>
        
int main()
{
    std::map<std::string, std::any> notebook;

    std::string name{ "Pluto" };
    int year = 2015;

    notebook["PetName"] = name;
    notebook["Born"] = year;

    std::string name2 = std::any_cast<std::string>(notebook["PetName"]); // = "Pluto"
    int year2 = std::any_cast<int>(notebook["Born"]); // = 2015
}
Tailpipe answered 20/6, 2018 at 19:55 Comment(1)
To simplify further, is there any way to replace int year2 = std::any_cast<int>(notebook["Born"]); with auto year2 = notebook["Born"]);?Pede
M
42

Blindy's answer is very good (+1), but just to complete the answer: there is another way to do it with no library, by using dynamic inheritance:

class MyFieldInterface
{
    int m_Size; // of course use appropriate access level in the real code...
    ~MyFieldInterface() = default;
}

template <typename T>
class MyField : public MyFieldInterface {
    T m_Value; 
}


struct MyClass {
    std::map<string, MyFieldInterface* > fields;  
}

Pros:

  • it's familiar to any C++ coder
  • it don't force you to use Boost (in some contexts you are not allowed to);

Cons:

  • you have to allocate the objects on the heap/free store and use reference semantic instead of value semantic to manipulate them;
  • public inheritance exposed that way might lead to over-use of dynamic inheritance and a lot of long-term issues related to your types really being too inter-dependent;
  • a vector of pointers is problematic if it have to own the objects, as you have to manage destruction;

So use boost::any or boost::variant as default if you can, and consider this option only otherwise.

To fix that last cons point you could use smart pointers:

struct MyClass {
    std::map<string, std::unique_ptr<MyFieldInterface> > fields;  // or shared_ptr<> if you are sharing ownership
}

However there is still a potentially more problematic point:

It forces you to create the objects using new/delete (or make_unique/shared). This mean that the actual objects are created in the free store (the heap) at any location provided by the allocator (mostly the default one). Therefore, going though the list of objects very often is not as fast as it could be because of cache misses.

diagram of vector of polymorphic objects

If you are concerned with performance of looping through this list very often as fast as possible (ignore the following if not), then you'd better use either boost::variant (if you already know all the concrete types you will use) OR use some kind of type-erased polymorphic container.

diagram of polymorphic container

The idea is that the container would manage arrays of objects of the same type, but that still expose the same interface. That interface can be either a concept (using duck-typing techniques) or a dynamic interface (a base class like in my first example). The advantage is that the container will keep same-type objects in separate vectors, so going through them is fast. Only going from one type to another is not.

Here is an example (the images are from there): http://bannalia.blogspot.fr/2014/05/fast-polymorphic-collections.html

However, this technique loose it's interest if you need to keep the order in which the objects are inserted.

In any way, there are several solutions possible, which depends a lot on your needs. If you have not enough experience with your case, I suggest using either the simple solution I first explained in my example or boost::any/variant.


As a complement to this answer, I want to point very good blog articles which summarize all C++ type-erasure techniques you could use, with comments and pros/cons:

Mylo answered 11/7, 2014 at 16:39 Comment(0)
G
20

Use either boost::variant (if you know the types you can store, it provides compile time support) or boost::any (for really any type -- but that's kind of unlikely to be the case).

http://www.boost.org/doc/libs/1_55_0/doc/html/variant/misc.html#variant.versus-any

Edit: I cannot emphasize enough that although rolling your own solution might seem cool, using a complete, proper implementation will save you a lot of headache in the long run. boost::any implements RHS copy constructors (C++11), both safe (typeid()) and unsafe (dumb casts) value retrievals, with const corectness, RHS operands and both pointer and value types.

That's true in general, but even more so for low level, base types you build your entire application on.

Griffey answered 11/7, 2014 at 16:21 Comment(0)
S
14
class AnyBase
{
public:
    virtual ~AnyBase() = 0;
};
inline AnyBase::~AnyBase() {}

template<class T>
class Any : public AnyBase
{
public:
    typedef T Type;
    explicit Any(const Type& data) : data(data) {}
    Any() {}
    Type data;
};

std::map<std::string, std::unique_ptr<AnyBase>> anymap;
anymap["number"].reset(new Any<int>(5));
anymap["text"].reset(new Any<std::string>("5"));

// throws std::bad_cast if not really Any<int>
int value = dynamic_cast<Any<int>&>(*anymap["number"]).data;
Spry answered 11/7, 2014 at 16:27 Comment(9)
You could add a member in the base class to get the value and avoid the extra casts ((Any<int> &)(*anymap["number"]).data looks horrible). But at that point you implemented a basic boost::any, might as well use the complete and well tested class. +1 though, the creating is spot on!Griffey
I'll definitely try this! Can you explain to me about the two destructors AnyBase has?? I don't quite understand why you have two of them and why you need them.. thanksFoti
@user3794186, There's only one desctructor, and it's virtual because otherwise derived types' desctructors wouldn't get called (and so the data destructors wouldn't get called).Griffey
@Griffey Oh I get it. What about the inline destructor..?Foti
@user3794186, it's just the implementation, pure virtual destructors have to have an implementation in C++. I think the point is to make the base class abstract (and so un-creatable), a clearer way would be with a private constructor.Griffey
Why does the AnyBase destructor definition needs to be inline?Arruda
@PedroBatista It doesn't have to be, but if it's not, its definition will have to be in a cpp file instead of the header file. So I made it inline for simplicity of example.Spry
@Griffey It needs to have a virtual destructor, regardless of how you define the constructor.Spry
@NeilKirk huge thank you for this answer, years later it helped me understand and solve a variant KeyValue pair solution for the Raspberry Pi RP2040. Gist here: gist.github.com/nstansbury/6d627a447ca3b4ebbcf2c49ae37922c5Supposal
G
5

C++17 has a std::variant type that has facilities for holding different types much better than a union.

For those not on C++17, boost::variant implements this same mechanism.

For those not using boost, https://github.com/mapbox/variant implements a much lighter version of variant for C++11 and C++14 that looks very promising, well documented, lightweight, and has plenty of usage examples.

Grahamgrahame answered 16/10, 2017 at 17:48 Comment(0)
D
2

You could also use a void* and cast the value back to the correct type using reinterpret_cast. Its a technique often used in C in callbacks.

#include <iostream>
#include <unordered_map>
#include <string>
#include <cstdint> // Needed for intptr_t
using namespace std;


enum TypeID {
    TYPE_INT,
    TYPE_CHAR_PTR,
    TYPE_MYFIELD
};    

struct MyField {
    int typeId;
    void * data;
};

int main() {

    std::unordered_map<std::string, MyField> map;

    MyField anInt = {TYPE_INT, reinterpret_cast<void*>(42) };

    char cstr[] = "Jolly good";
    MyField aCString = { TYPE_CHAR_PTR, cstr };

    MyField aStruct  = { TYPE_MYFIELD, &anInt };

    map.emplace( "Int", anInt );
    map.emplace( "C String", aCString );
    map.emplace( "MyField" , aStruct  );  

    int         intval   = static_cast<int>(reinterpret_cast<intptr_t>(map["Int"].data)); 
    const char *cstr2    = reinterpret_cast<const char *>( map["C String"].data );
    MyField*    myStruct = reinterpret_cast<MyField*>( map["MyField"].data );

    cout << intval << '\n'
         << cstr << '\n'
         << myStruct->typeId << ": " << static_cast<int>(reinterpret_cast<intptr_t>(myStruct->data)) << endl;
}
Donothing answered 11/7, 2014 at 16:46 Comment(1)
s/reinterpret_cast/static_cast/Brilliantine
H
1

This is a naive way of doing it. Of course, you can add wrappers to void the some boiler plate code.

#include <iostream>
#include <memory>
#include <map>
#include <vector>
#include <cassert>


struct IObject
{
    virtual ~IObject() = default;
};

template<class T>
class Object final : public IObject
{
public:
    Object(T t_content) : m_context(t_content){}
    ~Object() = default;

    const T& get() const
    {
        return m_context;
    }

private:
    T m_context;
};

struct MyClass
{
    std::map<std::string, std::unique_ptr<IObject>> m_fields;
};


int main()
{

    MyClass yourClass;

    // Content as scalar
    yourClass.m_fields["scalar"] = std::make_unique<Object<int>>(35);
    
    // Content as vector
    std::vector<double> v{ 3.1, 0.042 };
    yourClass.m_fields["vector"] = std::make_unique<Object<std::vector<double>>>(v);
       
    auto scalar = dynamic_cast<Object<int>*>(yourClass.m_fields["scalar"].get())->get();
    assert(scalar == 35);

    auto vector_ = dynamic_cast<Object<std::vector<double>>*>(yourClass.m_fields["vector"].get())->get();
    assert(v == vector_);

    return 0;
}
Hiltonhilum answered 30/10, 2021 at 11:10 Comment(0)
F
-3

Work in progress. The advantage this method has is that you don't have to cast anything when doing assignment, or any of the features listed below.

As of now it can:

  • store non-container literal types (const char*, double, int, float, char, bool)
  • output value for corresponding key with ostream operator
  • reassign the value of an existing key
  • add a new key:value pair using the append method only, key cannot be the same, or else you get an error message
  • add literals of the same type with the + operator

In the code, I have demonstrated in the main function what it can currently do.

/*
This program demonstrates a map of arbitrary literal types implemented in C++17, using any. 
*/

#include <vector>
#include <any>
#include <utility>
#include <iostream>
using namespace std;

class ArbMap
{
    public:
    ArbMap() : vec({}), None("None") {} //default constructor
    
    ArbMap(const vector < pair<any,any> > &x) //parametrized constructor, takes in a vector of pairs
    : vec(x), None("None") {}

    //our conversion function, this time we pass in a reference
    //to a string, which will get updated depending on which 
    //cast was successful. Trying to return values is ill-advised
    //because this function is recursive, so passing a reference
    //was the next logical solution
    void elem(any &x, string &temp, int num=0 )
    {
        try
        {
            switch (num)
            {
                case 0:
                any_cast<int>(x);
                temp = "i";
                break;
                case 1:
                any_cast<double>(x);
                temp = "d";
                break;
                case 2:
                any_cast<const char*>(x);
                temp = "cc";
                break;
                case 3:
                any_cast<char>(x);
                temp = "c";
                break;
                case 4:
                any_cast<bool>(x);
                temp = "b";
                break;
                case 5:
                any_cast<string>(x);
                temp = "s";
                break;
            }
        } 
        catch(const bad_cast& e)
        {
            elem(x,temp,++num);
        }
        
    }
    //returns size of vector of pairs 
    size_t size()
    {
        return vec.size();
    }


    /* Uses linear search to find key, then tries to cast 
    all the elements into the appropriate type. */
    any& operator[](any key)
    {
        ArbMap temp;        
        string stemp;
        for (size_t i = 0; i<vec.size(); ++i)
        {
            temp.elem(vec[i].first,stemp);
            if (stemp=="i")
            {
                try
                {
                    any_cast<int>(key);
                }
                catch(const bad_cast& e)
                {
                    continue;
                }
                if (any_cast<int>(key)==any_cast<int>(vec[i].first))
                {
                    return vec[i].second;
                }
            } 
            else if (stemp=="d")
            {
                try
                {
                    any_cast<double>(key);
                }
                catch(const bad_cast& e)
                {
                    continue;
                }
                if (any_cast<double>(key)==any_cast<double>(vec[i].first))
                {
                    return vec[i].second;
                }
            }
            else if (stemp=="cc")
            {
                try
                {
                    any_cast<const char*>(key);
                }
                catch(const bad_cast& e)
                {
                    continue;
                }
                if (any_cast<const char*>(key)==any_cast<const char*>(vec[i].first))
                {
                    return vec[i].second;
                }
            }
            else if (stemp=="c")
            {
                try
                {
                    any_cast<char>(key);
                }
                catch(const bad_cast& e)
                {
                    continue;
                }
                if (any_cast<char>(key)==any_cast<char>(vec[i].first))
                {
                    return vec[i].second;
                }
            }
            else if (stemp=="b")
            {
                try
                {
                    any_cast<bool>(key);
                }
                catch(const bad_cast& e)
                {
                    continue;
                }
                if (any_cast<bool>(key)==any_cast<bool>(vec[i].first))
                {
                    return vec[i].second;
                }
            }
        }
        //vec.push_back({key,None});
        throw -1;
        //return None;

    }
    void print();
    void append(any key, any value);
    private:
    vector < pair<any,any> > vec;
    any None;
};

ostream& operator<<(ostream& out, any a)
{
    ArbMap temp;        //should be updated to be a smart pointer?
    string stemp;
    temp.elem(a,stemp); //stemp will get updated in the elem function
    
    //The "if else-if ladder" for casting types
    if (stemp=="i") out << any_cast<int>(a); 
    else if (stemp=="d") out << any_cast<double>(a);
    else if (stemp=="cc") out << any_cast<const char*>(a);
    else if (stemp=="c") out << any_cast<char>(a);
    else if (stemp=="b") 
    {
        if (any_cast<bool>(a)==1)
        out << "true";
        else
        out << "false";
    }
    else if (stemp=="s") out << any_cast<string>(a);

    return out;
}

any operator+(any val1, any val2)
{
    ArbMap temp;        
    string stemp1, stemp2;
    temp.elem(val1,stemp1); 
    temp.elem(val2,stemp2); 

    try
    {
        if (stemp1 != stemp2)
            throw -1;

        if (stemp1 == "i")
        {
            return any_cast<int>(val1)+any_cast<int>(val2);
        }
        else if (stemp1 == "d")
        {
            return any_cast<double>(val1)+any_cast<double>(val2);
        }
        else if (stemp1 == "cc")
        {
            return string(any_cast<const char*>(val1))+string(any_cast<const char*>(val2));
        }
        else if (stemp1 == "c")
        {
            return string{any_cast<char>(val1)}+string{any_cast<char>(val2)};
        }
        else if (stemp1=="b")
        {
            return static_cast<bool>(any_cast<bool>(val1)+any_cast<bool>(val2));
        }


    }
    catch (int err)
    {
        cout << "Bad cast! Operands must be of the same 'type'.\n";
       
    }
    
    return val1;
    
}

void ArbMap::print()
{
    cout << '\n';
    for (size_t i = 0; i<vec.size(); ++i)
    {
        cout << vec[i].first << ": " << vec[i].second << '\n';
    }
    cout << '\n';
}

void ArbMap::append(any key, any value)
{
    try 
    {
        (*this)[key];
        throw "Already exists!";
        
    }
    catch(int error)
    {
        vec.push_back({key,value});
    }
    catch(const char* error)
    {
        cout << "ArbMap::append failed, key already exists!\n";
    }
    
}



int main() {
    ArbMap s({{1,2},{"aaa",1.2},{'c',33.3},{"what","this is awesome"}, {true, false}});
    
    cout << s[1] << '\n' << s["aaa"] << '\n' << s['c']
    << '\n' << s["what"] << '\n' 
    //Uncomment the line below and runtime error will occur, as 
    //entry is not in the dictionary
    // << s["not in the dictionary bro"] << '\n'
    << s[true] << '\n';
    
    
    s.print();
    s[1] = "hello";
    s.print();
    s.append(2.3,"what");
    s.print();
    s[2.3] = "hello";
    s.print();
    s.append(2.3,"what");
    s.print();
    s[1] = 1.2;
    s.print();
    s.append(2.4,1.2);
    
    //Operator +
    cout << s[1]+s[2.4] << '\n';
    cout << s["what"] + s[2.3] << '\n';
    s.append('d','a');
    cout << s['c'] << '\n';
    cout <<  s[2.4]+ s["aaa"]+ s['c'] + s['c'] + s['c'] << '\n';
    cout << s[true]+s[true] << '\n';

    return 0;
}
Favors answered 24/6, 2022 at 12:16 Comment(6)
What does "Work in progress..." mean?Haeckel
I don't know if you disliked it, but I think my solution is the only one that allows a map of arbitrary types without having to cast anything. It's a work in progress because it can't do everything that for example python dict can do, but I like to think it is a solid effort, certainly not worth a dislike.Favors
Please edit it in a way that what you wrote here as a comment, is also written in your answer above.Haeckel
Ok, I added some clarification.Favors
This looks wonky. If you only support a finite number of types, you should've been using std::variant. Also: Why is "using namespace std;" considered bad practice?.Leddy
Multiple types just haven't been implemented yet. Yes it's wonky, but C++ isn't Python, so I'm not sure what you people expect. If you have a better solution that achieves the same in C++ I'm happy to see it.Favors

© 2022 - 2024 — McMap. All rights reserved.