Question

For an std::map<std::string, std::string> variables, I'd like to do this:

BOOST_CHECK_EQUAL(variables["a"], "b");

The only problem is, in this context variables is const, so operator[] won't work :(

Now, there are several workarounds to this; casting away the const, using variables.count("a") ? variables.find("a")->second : std::string() or even making a function wrapping that. None of these seem to me to be as nice as operator[]. What should I do? Is there a standard way of doing this (beautifully)?

Edit: Just to state the answer that none of you want to give: No, there is no convenient, beautiful, standard way of doing this in C++. I will have to implement a support function.

Was it helpful?

Solution

template <typename K, typename V>
V get(std::map<K, V> const& map, K const& key)
{
    std::map<K, V>::const_iterator iter(map.find(key));
    return iter != map.end() ? iter->second : V();
}

Improved implementation based on comments:

template <typename T>
typename T::mapped_type get(T const& map, typename T::key_type const& key)
{
    typename T::const_iterator iter(map.find(key));
    return iter != map.end() ? iter->second : typename T::mapped_type();
}

OTHER TIPS

Casting away const is wrong, because operator[] on map<> will create the entry if it isn't present with a default constructed string. If the map is actually in immutable storage then it will fail. This must be so because operator[] returns a non-const reference to allow assignment. (eg. m[1] = 2)

A quick free function to implement the comparison:

template<typename CONT>
bool check_equal(const CONT& m, const typename CONT::key_type& k,
                    const typename CONT::mapped_type& v)
{
    CONT::const_iterator i(m.find(k));
    if (i == m.end()) return false;
    return i->second == v;
}

I'll think about syntactic sugar and update if I think of something.

...

The immediate syntactic sugar involved a free function that does a map<>::find() and returns a special class that wraps map<>::const_iterator, and then has overloaded operator==() and operator!=() to allow comparison with the mapped type. So you can do something like:

if (nonmutating_get(m, "key") == "value") { ... }

I'm not convinced that is much better than:

if (check_equal(m, "key", "value")) { ... }

And it is certainly much more complex and what is going on is much less obvious.

The purpose of the object wrapping the iterator is to stop having default constructed data objects. If you don't care, then just use the "get" answer.

In response to the comment about the get being preferred over a comparison in the hope of finding some future use, I have these comments:

  • Say what you mean: calling a function called "check_equal" makes it clear that you are doing an equality comparison without object creation.

  • I recommend only implementing functionality once you have a need. Doing something before then is often a mistake.

  • Depending on the situation, a default constructor might have side-effects. If you are comparing, why do anything extra?

  • The SQL argument: NULL is not equivalent to an empty string. Is the absence of a key from your container really the same as the key being present in your container with a default constructed value?

Having said all that, a default constructed object is equivalent to using map<>::operator[] on a non-const container. And perhaps you have a current requirement for a get function that returns a default constructed object; I know I have had that requirement in the past.

find is the idiomatic form. Casting away const is almost always a bad idea. You'd have to guarantee that no write operation is performed. While this can be reasonably expected of a read access on a map, the specification doesn't say anything about this.

If you know that the value exists you can of course forego the test using count (which is quite inefficient, anyway, since it means traversing the map twice. Even if you don't know whether the element exists I wouldn't use this. Use the following instead:

T const& item(map<TKey, T> const& m, TKey const& key, T const& def = T()) {
    map<TKey, T>::const_iterator i = m.find(key);
    return i == m.end() ? def : i->second;
}

/EDIT: As Chris has correctly pointed out, default-construction of objects of type T might be expensive, especially since this is done even if this object isn't actually needed (because the entry exists). If this is the case, don't use the default value for the def argument in the above case.

An interesting aside, there are two ways do the template type discovery in the get implementation that was accepted (the one that gets the value or returns a default constructed object). One, you can do what was accepted and have:

template <typename K, typename V>
V get1(const std::map<K, V>& theMap, const K const key)
{
    std::map<K, V>::const_iterator iter(theMap.find(key));
    return iter != theMap.end() ? iter->second : V();
}

or you can use the map type and get the types off of that:

template<typename T>
typename T::mapped_type
get2(const T& theMap, const typename T::key_type& key)
{
    typename T::const_iterator itr = theMap.find(key);
    return itr != theMap.end() ? itr->second : typename T::mapped_type();
}

The advantage of this is that the type of the key being passed in doesn't play in the type discovery and it can be something that can be implicitly converted to a key. For example:

std::map<std::string, int> data;
get1(data, "hey"); // doesn't compile because the key type is ambiguous
get2(data, "hey"); // just fine, a const char* can be converted to a string

Indeed, operator[] is a non-const one on std::map, since it automatically inserts a key-value pair in the map if it weren't there. (Oooh side-effects!)

The right way is using map::find and, if the returned iterator is valid (!= map.end()), returning the second, as you showed.

map<int, int> m;
m[1]=5; m[2]=6; // fill in some couples
...
map<int,int>::const_iterator it = m.find( 3 );
if( it != m.end() ) {
    int value = it->second;
    // ... do stuff with value
}

You could add a map::operator[]( const key_type& key ) const in a subclass of the std::map you're using, and assert the key to be found, after which you return the it->second.

std::map<std::string, std::string>::const_iterator it( m.find("a") );
BOOST_CHECK_EQUAL( 
                     ( it == m.end() ? std::string("") : it->second ), 
                     "b" 
                 );

That doesn't look too bad to me... I probably wouldn't write a function for this.

Following up xtofl's idea of specializing the map container. Will the following work well?

template <typename K,typename V>  
struct Dictionary:public std::map<K,V>  
{  
  const V& operator[] (const K& key) const  
  {  
    std::map<K,V>::const_iterator iter(this->find(key));  
    BOOST_VERIFY(iter!=this->end());  
    return iter->second;  
  }  
};  
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top