C++ unordered_map default allocator

Viewed 206

I have an abstract class (parent) and just created derived class (child) from it as below:

class parent {
public:
    virtual string print()=0;
};

template<class T>
class child: public parent{
private:
    unordered_map<string, parent*> children;
public:
    parent *&operator[](string name) {
        return children[name];
    }
    virtual string print(){
        //some jobs...
    }
}

ok. Now i used derived class like below:

child<string> a;
a["test"]->print();

and the problem shows out.

I think the problem is that unordered_map::[] operator creates an instance of parent class rather than child class that I want.

How can tell unordered_map to create new items as child < string >

2 Answers

Suppose you have an std::unordered_map<std::string, int> and you do the following operations:

std::unordered_map<std::string, int> m;
std::cout << m["hello"];
std::cout << m["key1"];

The above program will not produce any kind of warning or error. In fact, it will output 00. Two zeroes. So, what is happening here?

unordered_map or map operator[] inserts a value in case the key does not exist. This is one of the biggest source of unintentional bugs in C++. In the case above "hello" or "key1" did not exist, so they were created in-place with the value default initialized to 0.

How to solve your issue?

Always make sure before using the operator[] whether the value you are trying to access even exists or not. In fact, it is just better to use the .at() member function if you want to perform a read.

Fixed code:

child<string> a;
a["test"] = new child<string>;
a["test"]->print();

You can further prevent this behaviour by changing your operator[] function:

    parent *&operator[](string name) {
        if (children.find(name) == children.end()) // does it exist?
            children[name] = new child<T>; // if it doesn't, create a new one.
        return children[name];
    }

unordered_map<std::string, parent *>::operator[] won't create instances of any class, as its values are pointers, not class instances. So if you use it to access a key that was not previously inserted in the map, it will default-initialize a pointer (which initializes it to nullptr) and store that in the map.

If you want any non-nullptr values in the map, you'll need to store them there yourself. You can do that in your operator[] function:

parent *&operator[](string name) {
    auto &rv = children[name];
    if (!rv) rv = new child<T>;
    return rv;
}

This has the danger of leaking memory if you ever overwrite any of these pointers that are returned by reference, plus you need to free them all in the destructor. You can avoid the former problem by returning a parent * instead of a parent *&, or by using std::unique_ptr<parent> in the map and returning a std::unique_ptr<parent> & here.

Related