How to create a std::map of constant values which is still accessible by the [] operator?

21,385

Solution 1

Couldn't you use the insert() method available for std::map?

http://www.cplusplus.com/reference/map/map/insert/

Edit : (solution) myMap.insert(std::pair<std::string, const int>("Keys", 42));

As far as I understand it, the reason why this works is because the constructor for the pair, pair (const first_type& a, const second_type& b), initializes its members first and second using the constructors for first_type and second_type, taking a and b as their respective parameter.

With the solution you were trying to use, my comprehension is that myMap["Keys"] = 42; initializes the member second of the map (of type const int) using the default constructor for int. Then a value is attempted to be assigned to that member. As this is done outside the constructor of the class map, the const declaration makes this impossible.

With the solution using insert(), the members are initialized in the constructor of the pair. Thus they can be declared const. The same operation is done when the pair is copied to the map.

Solution 2

While this is not possible for you, others who wants to do this and that have a C++11 compatible compiler, could use uniform initialization:

std::map<std::string, const int> myMap = {
    { "keys", 42 }
};

Oh and by the way, don't define the map in the header file. Instead declare it as extern in the header file, then define it in the source file.

Solution 3

The simplest solution is to write your own, wrapping the standard map class:

template <typename KeyType, typename MappedType, typename CmpType>
class ConstantMap
{
    typedef std::map<KeyType, MappedType, CmpType> Impl;
    Impl myImpl;
public:
    typedef Impl::value_type value_type;

    template <ForwardIterator>
    ConstantMap( ForwardIterator begin, ForwardIterator end, CmpType cmp = CmpType() )
        : myImpl( begin, end, cmp )
    {
    }

    //  necessary if [] is not going to work for missing keys
    bool contains( KeyType const& key ) const
    {
        return myImpl.find( key ) != myImpl.end();
    }

    MappedType const& operator[]( KeyType const& key ) const
    {
        Impl::const_iterator elem = myImpl.find( key );
        if ( elem == myImpl.end() ) {
            //  Not found, do what you want (maybe throw an exception)
        }
        return elem.second;
    }
};

You can initialze the map by passing it iterators to a sequence of anything which can convert to value_type.

Depending on your needs, you may want to add additional forwarding typedefs, functions, etc. If you're using C++11, you may also want to create a constructor which can use a list initializer.

Solution 4

If the map can't mutate, you should use const map<string, int>, and not map<string, const int>: the second version allows insertion and deletion of objects.

Sadly, you are going to have to lose the [] operator; C++ doesn't have an ImmutableMap, or something like that. However, std::map::at and std::map::find aren't too bad...

Share:
21,385
Chaos_99
Author by

Chaos_99

@Chaos_99

Updated on July 09, 2022

Comments

  • Chaos_99
    Chaos_99 almost 2 years

    I need a std:map data structure that is read only, which means I have to fill it once with data and then only read those values, never change them or add additional ones.

    My non-const version looks like this:

    //in .h
    #include <string>
    #include <map>
    
    std::map<std::string, int> myMap;
    void initMap();
    
    //in .cpp
    #include "foo.h"
    
    void initMap() {
      myMap["Keys"] = 42;
    }
    

    Then I'd call initMap() once in my code and be done.

    Now I've read several questions here already and it seems non-trivial to achieve const-ness for the map.

    Making it a std::map<std::string, const int> won't allow me to fill it in the initMap(). Filling it with a non-const temp and the copy constructor on definition doesn't work either, as the copy constructor doesn't easily take the non-const version as input.

    Making it a const std::map<std::string, int> (which I could fill with a non-const copy during definition) would disable the use of the [] operator for value access.

    So is there a way to achieve (value) const-ness and initialize the structure (preferably in the header file)?

    BTW: Neither C++0x nor C++11 nor boost:: is an option.

  • Chaos_99
    Chaos_99 almost 11 years
    If it's between the [] operator and insertion/deletion, I'd go with insertion/deletion. Not using something by convention is easier to 'sell' to other devs then clunky access syntax.
  • Chaos_99
    Chaos_99 almost 11 years
    I've edited the original question. No C++0x also means no C++11. But you're absolutely right about the declare/define. Thanks!
  • Chaos_99
    Chaos_99 almost 11 years
    Expand your answer to be way more verbatim and actually answer my question and I accept is as right. Indeed, using my own code example above, replacing myMap["Keys"] = 42; by myMap.insert(std::make_pair("Keys", 42)) will work for a std::map<std::string, const int>. But I have no idea why this one works and the other one not.
  • Chaos_99
    Chaos_99 almost 11 years
    And I just verified, it correctly gives me a compiler error on write access (myMap["Keys"] = 23;) while still allowing read access (std::cout << myMap["Keys"]).
  • Chaos_99
    Chaos_99 almost 11 years
    Thanks for the wrapper class example code. I'm sure it may come handy to a lot of people searching for a custom map class. But I'll take @Matzar s solution as long as const-ness is my only need.
  • Matzar
    Matzar almost 11 years
    @Chaos_99 Sorry. I would've preferred to write it as a comment, but I'm not yet allowed to.
  • Matzar
    Matzar almost 11 years
    @Chaos_99 I added more in-depth explanations to my answer. This applies to the solution that I gave as an example. If you use myMap.insert(std::make_pair("Keys", 42)), the principle is the same, but the initialization of the const int is only done in the constructor of map, since the type of the members of the pair will be whatever it decides is most fitting for "Keys" and 42.
  • Chaos_99
    Chaos_99 almost 7 years
    Might work for this simple example, but running a switch statement with thousands of options is probably not faster than a std::map access.