Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ associative array with arbitrary types for values

What is the best way to have an associative array with arbitrary value types for each key in C++?

Currently my plan is to create a "value" class with member variables of the types I will be expecting. For example:

class Value {

    int iValue;
    Value(int v) { iValue = v; }

    std::string sValue;
    Value(std::string v) { sValue = v; }

    SomeClass *cValue;
    Value(SomeClass *v) { cValue = c; }

};

std::map<std::string, Value> table;

A downside with this is you have to know the type when accessing the "Value". i.e.:

table["something"] = Value(5);
SomeClass *s = table["something"].cValue;  // broken pointer

Also the more types that are put in Value, the more bloated the array will be.

Any better suggestions?

like image 625
gak Avatar asked Dec 29 '08 10:12

gak


5 Answers

boost::variant seems exactly what you are looking for.

like image 174
Marc Avatar answered Oct 31 '22 22:10

Marc


Your approach was basically into the right direction. You will have to know the type you put into. You can use boost::any and you will be able to put just about anything into the map, as long as you know what you put into:

std::map<std::string, boost::any> table;
table["hello"] = 10;
std::cout << boost::any_cast<int>(table["hello"]); // outputs 10

Some answers recommended the use of boost::variant to solve this problem. But it won't let you store arbitrary typed values in the map (like you wanted). You have to know the set of possible types before-hand. Given that, you can do the above more easily:

typedef boost::variant<int, std::string, void*> variant_type;
std::map<std::string, variant_type> table;
table["hello"] = 10;
// outputs 10. we don't have to know the type last assigned to the variant
// but the variant keeps track of it internally.
std::cout << table["hello"];

That works because boost::variant overloads operator<< for that purpose. It's important to understand that if you want to save what is currently contained in the variant, you still have to know the type, as with in the boost::any case:

typedef boost::variant<int, std::string, void*> variant_type;
std::map<std::string, variant_type> table;
table["hello"] = "bar";
std::string value = boost::get<std::string>(table["hello"]);

The order of assignments to a variant is a runtime property of the control flow of your code, but the type used of any variable is determined at compile time. So if you want to get the value out of the variant, you have to know its type. An alternative is to use visitation, as outlined by the variant documentation. It works because the variant stores a code which tells it which type was last assigned to it. Based on that, it decides at runtime which overload of the visitor it uses. boost::variant is quite big and is not completely standard compliant, while boost::any is standard compliant but uses dynamic memory even for small types (so it's slower. variant can use the stack for small types). So you have to trade off what you use.

If you actually want to put objects into it which differ only in the way they do something, polymorphism is a better way to go. You can have a base-class which you derive from:

std::map< std::string, boost::shared_ptr<Base> > table;
table["hello"] = boost::shared_ptr<Base>(new Apple(...));
table["hello"]->print();

Which would basically require this class layout:

class Base {
public:
    virtual ~Base() { }
    // derived classes implement this:
    virtual void print() = 0;
};

class Apple : public Base {
public:
    virtual void print() {
        // print us out.
    }
};

The boost::shared_ptr is a so-called smart pointer. It will delete your objects automatically if you remove them out of your map and nothing else is referencing them them anymore. In theory you could have worked with a plain pointer too, but using a smart pointer will greatly increase safety. Read the shared_ptr manual i linked to.

like image 23
Johannes Schaub - litb Avatar answered Oct 31 '22 23:10

Johannes Schaub - litb


Subclass Value with IntValue, StringValue, and so on.

like image 2
Bombe Avatar answered Oct 31 '22 22:10

Bombe


Can you use a union with std::map?

Boost::variant provides typeless variables.

Altrnatively you could make all your Value data members private and provide accessors that return an error (or throw) if it isn't set.

like image 2
Patrick Avatar answered Oct 31 '22 21:10

Patrick


A straight-forward optimisation would be to use a union, since you'll always only have one of the values as key.

A more complete solution would encapsulate some run time type information into a interface. Primarily "Which type is this?" and "How do I compare values for equality?" Then use implementations of that as key.

like image 1
David Schmitt Avatar answered Oct 31 '22 23:10

David Schmitt