views:

1532

answers:

5

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?

+2  A: 

Subclass Value with IntValue, StringValue, and so on.

Bombe
+5  A: 

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.

Johannes Schaub - litb
That's the runtime-type-checking option. For a compile-time-type-checking option, there's also boost::variant (use with visitor pattern for a watertight static type-checked solution).
Chris Jester-Young
well, we talked about that in irc i think :) just for all others reading it: variant can only store a limited set of types, and if one wants to get a value out of an entry, one can use visitation (and will get at the value without knowing its type), but one cannot save the value anywhere.
Johannes Schaub - litb
because the decision what version of the visitor is called is done at runtime. the type of a variable, however, is determined at compile time. so you can't do SomeAutoDeducedType result = boost::apply_visitor( my_visitor(), u ); . all op() of a visitor got the same return type.
Johannes Schaub - litb
+2  A: 

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.

Patrick
You can use an union with std::map, but unions can only hold POD-type objects.
mstrobl
+1  A: 

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.

David Schmitt
Unions can't hold std::string's
MSalters
Indeed, g++ complains "error: member ‘std::string [...]’ with constructor not allowed in union" (amongst other things). My C++ is getting rusty :-/
David Schmitt
+11  A: 

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

Marc