Search code examples
c++unordered-mapaccessordefault-constructor

Why does the method getNodeSuccess work, but not getNodeFail?


I am confused regarding the behavior of the following code: I don't understand why the getNodeSuccess method works, but the getNodeFail method fails to build. The only difference between those two methods is that one uses the [] operator for accessing the map, and the other one the at method. I thought that both these operations were returning a reference to the element in the map, and both should basically do the same thing. However, when I use the [] operator instead of the at()method, it tries to call the default constructor of the GraphNode class. Since the default constructor is private, the build fails.

For demonstration, I've made the default constructor of the GraphNode class private. If I change it to public, than it works.

Can somebody explain what's going on here? Why does one method trigger the default constructor, and not the other one?

#include <vector>
#include <list>
#include <unordered_map>
#include <exception>

template <class T>
struct GraphNode
{
    int id;
    T data;
    std::list<int> adj; // adjacency list

    GraphNode(const int id) 
    {
        this->id = id;
    }
private:
    GraphNode() {}
};

template<class T>
struct UndirectedGraph 
{
    std::unordered_map<int, GraphNode<T>> lookup;
    std::vector<GraphNode<T>> vertices;

    void addNode(const GraphNode<T> node) 
    {
        // if the id of the node already exists, than we cannot add it
        if (lookup.find(node.id) == lookup.end())
            lookup.insert(std::make_pair(node.id, node));
        else
            throw std::runtime_error("Trying to add a node that already exists.");
    }

    void addEdge(const int source, const int destination) 
    {
        getNodeSuccess(source).adj.push_back(destination);
        getNodeSuccess(destination).adj.push_back(source);

        getNodeFail(source).adj.push_back(destination);
        getNodeFail(destination).adj.push_back(source);
    }

    GraphNode<T>& getNodeSuccess(const int id)
    {
        if (lookup.find(id) == lookup.end())
            throw std::runtime_error("Trying to retrieve a node that doesn't exist");

        return lookup.at(id);
    }

    GraphNode<T>& getNodeFail(const int id)
    {
        if (lookup.find(id) == lookup.end())
            throw std::runtime_error("Trying to retrieve a node that doesn't exist");

        return lookup[id];
    }
};

int main(int argc, char* argv[]) 
{
    UndirectedGraph<int> graph;

    graph.addNode(GraphNode<int>(1));
    graph.addNode(GraphNode<int>(3));
    graph.addEdge(1, 3);

    return 0;
}

Solution

  • operator[] will either return a reference to the value with that key, or create one using the default constructor, insert it, and return a reference to the newly constructed value. Thus, your problem is that operator[] must be able to at least call the default constructor, even if you know the key will always be present.

    If you just want to lookup whether something is in your unordered_map, your use of find is already close.

    GraphNode<T>& getNodeFail(const int id)
    {
        auto it = lookup.find(id);
        if (it == lookup.end())
            throw std::runtime_error("Trying to retrieve a node that doesn't exist");
        return it->second;
    }
    

    That will save you an extra lookup as well, since you already did the work once with find.