KansaiRobot
KansaiRobot

Reputation: 9912

passing const as this argument discards qualifiers only when using a unordered_map but not a vector

I understand that calling a non-const method to a constant object gives an error as explained here. This question, although deals with the same error, is not a duplicate because it is not about a non-constant method.

This time I have a minimal reproducible example:

// Example program
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>

class Something
{
public:
    int m_value;
    Something(): m_value{0} { myVector.push_back(1); myMap["hello"]=3; }
    void setValue(int value) { m_value = value; }
    int getValue() { return m_value ; }
    //int getValue(const int value){ return myVector[value]  ;  }  //<-- this gives an error (just reference)
    int getValue(const int value)const { return myVector[value];    }
    //int getValue2(const std::string &name) {return myMap[name];   }  //<--- this gives an error (just reference)
    int getValue2(const std::string &name) const {return myMap[name];   } //HERE this gives an error (this question)
    
   std::vector<int> myVector;
   std::unordered_map<std::string,int> myMap;
   
};
 
int main()
{
    const Something something{}; // calls default constructor
                
    int l= something.getValue(0);
    std::cout<<l<<std::endl;
    
    l= something.getValue2("hello");  //<-- HERE the error
    std::cout<<l<<std::endl;
    
    return 0;
}

In comments there are two method declarations that illustrates the point about non-constant methods. I left them there for reference. This question is not above them.

You see the const getValue method that returns a vector element? This works without problem. Now see the const getValue2 method that should return a unordered map element? Even though it is a constant method it generates an error

In member function 'int Something::getValue2(const string&) const':
17:68: error: passing 'const std::unordered_map<std::basic_string<char>, int>' as 'this' argument of 'std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::mapped_type& std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::operator[](const key_type&) [with _Key = std::basic_string<char>; _Tp = int; _Hash = std::hash<std::basic_string<char> >; _Pred = std::equal_to<std::basic_string<char> >; _Alloc = std::allocator<std::pair<const std::basic_string<char>, int> >; std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::mapped_type = int; std::unordered_map<_Key, _Tp, _Hash, _Pred, _Alloc>::key_type = std::basic_string<char>]' discards qualifiers [-fpermissive]

My question is: Why only with unordered maps passing constant as the index generates this error?

EDIT: Thanks to the very useful answers. I modified the class to

// Example program
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>

class Something
{
public:
    int m_value;
    Something(): m_value{0} { myVector.push_back(1); myMap["hello"]=3; }
    void setValue(int value) { m_value = value; }
    int getValue() { return m_value ; }
    //int getValue(const int value){ return myVector[value]  ;  }  //<-- this gives an error
    int getValue(const int value)const { return myVector[value];    }
    //int getValue2(const std::string &name) {return myMap[name];   }  //<--- this gives an error
   //this will generate an exception if name is not in the map
    int getValue3(const std::string &name) const {
              //return myMap[name]; 
                return myMap.at(name);}
    int getValue2(const std::string &name) const {
            auto iter = myMap.find(name);
            return (iter != myMap.end()) ? iter->second : 0;
}
    
    
   std::vector<int> myVector;
   std::unordered_map<std::string,int> myMap;
   
};

Upvotes: 4

Views: 3272

Answers (3)

463035818_is_not_an_ai
463035818_is_not_an_ai

Reputation: 122238

A std::vector either has an element at a given index or the index is out-of-bounds, in that case a call to its operator[] invokes undefined behavior.

On the other hand, with the key alone you cannot determine if a std::unordered_map has a value for that key or not. You have to search the map.

To find if a map has a value for a key and use that value:

std::unordered_map<int,int> x;
auto it = x.find(3);
if (it == x.end()) {
    // element not found
} else {
    auto value = it->second;
}

operator[] does more than that. If the element was not found it does insert a element with default constructed value for the given key and returns a reference to the newly inserted value. It is more or less the same as:

std::unordered_map<int,int> x;
auto it = x.find(3);
if (it == x.end()) {
    it = x.emplace(std::make_pair(3,0).first;
                                   // .second is bool to 
                                   // indicate wether an element
                                   // was actually inserted.
                                   // In this case we know it wasn't present before
}
auto value = it->second;
// or simpler:
auto value = x[3];

The benefit of operator[] is that you get a valid element in any case, but the price is that operator[] cannot be const.

TL;DR

Accessing a vector out-of-bounds is to be avoided. std::vector::operator[] always returns an existing element (or invokes undefined behavior). On the other hand, looking up elements in a std::unordered_map that are not present is common. In that case you need to choose what to do when the element was not present. One option is operator[] that potentially inserts an element in the map, hence cannot be const.

Upvotes: 0

Remy Lebeau
Remy Lebeau

Reputation: 595837

something is a const object, so getValue(int) and getValue2(string) need to be const-qualified in order to be callable on it. That means the this pointer inside of them will always be pointing at a const object, so all operations on the object's data members need to be const-qualified as well.

Something::getValue(int) calls myVector[value], which works OK because std::vector::operator[] has a const-qualified overload to provide read-only access to the elements of a const std::vector object.

On the other hand, Something::getValue2(string) calls myMap[name], which does not work because std::unordered_map::operator[] is not const-qualified at all, so it can't be called on a const unordered_map object. A std::(unordered_)map's operator[] performs an insertion operation if the specified key is not found, thus the std::(unordered_)map object can't be const when using operator[]. To read an element from a const std::(unordered_)map without inserting a new element, you have to use the map's find() method instead, which is const-qualified, eg:

int getValue2(const std::string &name) const {
    auto iter = myMap.find(name);
    return (iter != myMap.end()) ? iter->second : 0;
}    

Upvotes: 4

Brian Bi
Brian Bi

Reputation: 119124

The const-qualified version of getValue2 only has const access to the members of Something. This means that it will see myMap with the type const std::unordered_map<std::string,int> and you cannot call any non-const member functions on myMap. The operator[] is a non-const member function (it cannot be made const, because it sometimes has to insert a value-initialized entry, namely when the key is not found in the map) so you get the error message about discarding qualifiers. To get around this, you can use .at(name) instead of [name]. This will throw an exception if name is not found in the map.

Upvotes: 10

Related Questions