Is there a way to specify the default value std::map's operator[] returns when an key does not exist?
Is there a way to specify the default value std::map's operator[] returns when an key does not exist?
No, there isn't. The simplest solution is to write your own free template function to do this. Something like:
#include <string>
#include <map>
using namespace std;
template <typename K, typename V>
V GetWithDef(const std::map <K,V> & m, const K & key, const V & defval ) {
typename std::map<K,V>::const_iterator it = m.find( key );
if ( it == m.end() ) {
return defval;
}
else {
return it->second;
}
}
int main() {
map <string,int> x;
...
int i = GetWithDef( x, string("foo"), 42 );
}
C++11 Update
Purpose: Account for generic associative containers, as well as optional comparator and allocator parameters.
template <template<class,class,class...> class C, typename K, typename V, typename... Args>
V GetWithDef(const C<K,V,Args...>& m, K const& key, const V & defval)
{
typename C<K,V,Args...>::const_iterator it = m.find( key );
if (it == m.end())
return defval;
return it->second;
}
The C++ standard (23.3.1.2) specifies that the newly inserted value is default constructed, so map itself doesn't provide a way of doing it. Your choices are:
operator[] to insert that default.There is no way to specify the default value - it is always value constructed by the default (zero parameter constructor).
In fact operator[] probably does more than you expect as if a value does not exist for the given key in the map it will insert a new one with the value from the default constructor.
One workaround is to use map::at() instead of [].
If a key does not exist, at throws an exception.
Even nicer, this also works for vectors, and is thus suited for generic programming where you may swap the map with a vector.
Using a custom value for unregistered key may be dangerous since that custom value (like -1) may be processed further down in the code. With exceptions, it's easier to spot bugs.
Pre-C++17, use std::map::insert(), for newer versions use try_emplace(). It may be counter-intuitive, but these functions effectively have the behaviour of operator[] with custom default values.
Realizing that I'm quite late to this party, but if you're interested in the behaviour of operator[] with custom defaults (that is: find the element with the given key, if it isn't present insert a chosen default value and return a reference to either the newly inserted value or the existing value), there is already a function available to you pre C++17: std::map::insert(). insert will not actually insert if the key already exists, but instead return an iterator to the existing value.
Say, you wanted a map of string-to-int and insert a default value of 42 if the key wasn't present yet:
std::map<std::string, int> answers;
int count_answers( const std::string &question)
{
auto &value = answers.insert( {question, 42}).first->second;
return value++;
}
int main() {
std::cout << count_answers( "Life, the universe and everything") << '\n';
std::cout << count_answers( "Life, the universe and everything") << '\n';
std::cout << count_answers( "Life, the universe and everything") << '\n';
return 0;
}
which should output 42, 43 and 44.
If the cost of constructing the map value is high (if either copying/moving the key or the value type is expensive), this comes at a significant performance penalty, which would be circumvented with C++17's try_emplace().
If you have access to C++17, my solution is as follows:
std::map<std::string, std::optional<int>> myNullables;
std::cout << myNullables["empty-key"].value_or(-1) << std::endl;
This allows you to specify a 'default value' at each use of the map. This may not necessarily be what you want or need, but I'll post it here for the sake of completeness. This solution lends itself well to a functional paradigm, as maps (and dictionaries) are often used with such a style anyway:
Map<String, int> myNullables;
print(myNullables["empty-key"] ?? -1);
With C++20 it is simple to write such getter:
constexpr auto &getOrDefault(const auto &map, const auto &key, const auto &defaultValue)
{
const auto itr = map.find(key);
return itr == map.cend() ? defaultValue : itr->second;
}
Maybe you can give a custom allocator who allocate with a default value you want.
template < class Key, class T, class Compare = less<Key>,
class Allocator = allocator<pair<const Key,T> > > class map;
Expanding on the answer https://stackoverflow.com/a/2333816/272642, this template function uses std::map's key_type and mapped_type typedefs to deduce the type of key and def.
This doesn't work with containers without these typedefs.
template <typename C>
typename C::mapped_type getWithDefault(const C& m, const typename C::key_type& key, const typename C::mapped_type& def) {
typename C::const_iterator it = m.find(key);
if (it == m.end())
return def;
return it->second;
}
This allows you to use
std::map<std::string, int*> m;
int* v = getWithDefault(m, "a", NULL);
without needing to cast the arguments like std::string("a"), (int*) NULL.
Here is a correct approach that will conditionally return a reference if the caller passes in an lvalue reference to the mapped type.
template <typename Map, typename DefVal>
using get_default_return_t = std::conditional_t<std::is_same_v<std::decay_t<DefVal>,
typename Map::mapped_type> && std::is_lvalue_reference_v<DefVal>,
const typename Map::mapped_type&, typename Map::mapped_type>;
template <typename Map, typename Key, typename DefVal>
get_default_return_t<Map, DefVal> get_default(const Map& map, const Key& key, DefVal&& defval)
{
auto i = map.find(key);
return i != map.end() ? i->second : defval;
}
int main()
{
std::map<std::string, std::string> map;
const char cstr[] = "world";
std::string str = "world";
auto& ref = get_default(map, "hello", str);
auto& ref2 = get_default(map, "hello", std::string{"world"}); // fails to compile
auto& ref3 = get_default(map, "hello", cstr); // fails to compile
return 0;
}
If you would like to keep using operator[] just like when you don't have to specify a default value other than what comes out from T() (where T is the value type), you can inherit T and specify a different default value in the constructor:
#include <iostream>
#include <map>
#include <string>
int main() {
class string_with_my_default : public std::string {
public:
string_with_my_default() : std::string("my default") {}
};
std::map<std::string, string_with_my_default> m;
std::cout << m["first-key"] << std::endl;
}
However, if T is a primitive type, try this:
#include <iostream>
#include <map>
#include <string>
template <int default_val>
class int_with_my_default {
private:
int val = default_val;
public:
operator int &() { return val; }
int* operator &() { return &val; }
};
int main() {
std::map<std::string, int_with_my_default<1> > m;
std::cout << m["first-key"] << std::endl;
++ m["second-key"];
std::cout << m["second-key"] << std::endl;
}