129

Is there a way to specify the default value std::map's operator[] returns when an key does not exist?

1
  • Sounds like std::map could definitely use a ::value_or function Commented Aug 22, 2024 at 22:41

16 Answers 16

83

While this does not exactly answer the question, I have circumvented the problem with code like this:

struct IntDefaultedToMinusOne
{
    int i = -1;
};

std::map<std::string, IntDefaultedToMinusOne > mymap;
Sign up to request clarification or add additional context in comments.

3 Comments

This is the best solution for me. Easy to implement, very flexible, and generic.
note you can add operator int() const {return i;}, which allows you to treat it as an int in most cases.
@acegs You could make it actually generic: template <typename T> struct default { T t; }; Then you can think about adding conversion operator and converting constructor
71

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>
const 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;
}

9 Comments

Nice solution. You might want to add a few template arguments so that the function template works with maps that don't use the default template parameters for comparator and allocator.
+1, but to provide the exact same behavior as the operator[] with default value, the default value should be inserted into the map inside the if ( it == m.end() ) block
@David I'm assuming the OP doesn't actually want that behaviour. I use a similar scheme for reading configurations, but I don't want the configuration updated if a key is missing.
@GMan bool parameters are thought by some to be bad style because you can't tell by looking at the call (as opposed to the declaration) what they do - in this case does "true" mean "use default" or "don't use default" (or something else entirely)? An enum is always clearer but of course is more code. I'm in two minds on the subject, myself.
This answer doesn't work if the default value is nullptr, but stackoverflow.com/a/26958878/297451 does.
|
16

C++17 provides try_emplace which does exactly this. It takes a key and an argument list for the value constructor and returns a pair: an iterator and a bool.: http://en.cppreference.com/w/cpp/container/map/try_emplace

3 Comments

That's not exactly the same thing, since it modifies the map.
Isn't it? The question was "Is there a way to specify the default value std::map's operator[] returns when an key does not exist?" That implies they are calling the non-const version of operator[] which does (potentially) modify the map.
@Ben : IMHO the OP did not intend actually to modify the map but rather to have an equivalent of python Dict get(key,defaultValue). Definitly there is a missing API in the std to provides an overload at function for such case
13

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:

  • Give the value type a default constructor that initialises it to the value you want, or
  • Wrap the map in your own class that provides a default value and implements operator[] to insert that default.

1 Comment

Well, to be precise the newly inserted value is value initialized (8.5.5) so: - if T is a class type with a user-declared constructor (12.1), then the default constructor for T is called (and the initialization is ill-formed if T has no accessible default constructor); — if T is a non-union class type without a user-declared constructor, then every non-static data member and baseclass component of T is value-initialized; — if T is an array type, then each element is value-initialized; — otherwise, the object is zero-initialized
10

The value is initialized using the default constructor, as the other answers say. However, it is useful to add that in case of simple types (integral types such as int, float, pointer or POD (plan old data) types), the values are zero-initialized (or zeroed by value-initialization (which is effectively the same thing), depending on which version of C++ is used).

Anyway, the bottomline is, that maps with simple types will zero-initialize the new items automatically. So in some cases, there is no need to worry about explicitly specifying the default initial value.

std::map<int, char*> map;
typedef char *P;
char *p = map[123],
    *p1 = P(); // map uses the same construct inside, causes zero-initialization
assert(!p && !p1); // both will be 0

See Do the parentheses after the type name make a difference with new? for more details on the matter.

Comments

6

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.

2 Comments

Right, to avoid adding new entries you could use find which does return the end iterator if no element exists for a given key.
@ThomasSchaub How much is the time complexity of find in that case?
5
template<typename T, T X>
struct Default {
    Default () : val(T(X)) {}
    Default (T const & val) : val(val) {}
    operator T & () { return val; }
    operator T const & () const { return val; }
    T val;
};

<...>

std::map<KeyType, Default<ValueType, DefaultValue> > mapping;

1 Comment

Then modify it so it does work. I'm not going to bother to fix a case this code was not designed to accomplish.
4

More General Version, Support C++98/03 and More Containers

Works with generic associative containers, the only template parameter is the container type itself.

Supported containers: std::map, std::multimap, std::unordered_map, std::unordered_multimap, wxHashMap, QMap, QMultiMap, QHash, QMultiHash, etc.

template<typename MAP>
const typename MAP::mapped_type& get_with_default(const MAP& m, 
                                             const typename MAP::key_type& key, 
                                             const typename MAP::mapped_type& defval)
{
    typename MAP::const_iterator it = m.find(key);
    if (it == m.end())
        return defval;

    return it->second;
}

Usage:

std::map<int, std::string> t;
t[1] = "one";
string s = get_with_default(t, 2, "unknown");

Here is a similar implementation by using a wrapper class, which is more similar to the method get() of dict type in Python: https://github.com/hltj/wxMEdit/blob/master/src/xm/xm_utils.hpp

template<typename MAP>
struct map_wrapper
{
    typedef typename MAP::key_type K;
    typedef typename MAP::mapped_type V;
    typedef typename MAP::const_iterator CIT;

    map_wrapper(const MAP& m) :m_map(m) {}

    const V& get(const K& key, const V& default_val) const
    {
        CIT it = m_map.find(key);
        if (it == m_map.end())
            return default_val;

        return it->second;
    }
private:
    const MAP& m_map;
};

template<typename MAP>
map_wrapper<MAP> wrap_map(const MAP& m)
{
    return map_wrapper<MAP>(m);
}

Usage:

std::map<int, std::string> t;
t[1] = "one";
string s = wrap_map(t).get(2, "unknown");

1 Comment

MAP::mapped_type& is not safe to return since typename MAP::mapped_type& defval can be out of scope.
3

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().

Comments

2

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.

Comments

2

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.

Comments

2

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);

4 Comments

This assumes the map itself contains raw optionals, which isn't always the case.
@Zoe I'm not sure what you're suggesting. My solution is to create a map using an optional of T as the value. If you are writing the code from scratch (or modifying existing code), you can create a map of raw optionals any time you like (with some restrictions, e.g. no optional references)
But it doesn't work if the map is out of your control - that was my point. It's not a general solution that works for any arbitrary map
@Zoe Ah, now I understand. Thank you for the input and clarification.
2

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;
}

As @425nesp and КоеКто noticed, the compiler won't compile code with undefined behavior only if getOrDefault called as constant expression. If getOrDefault called as regular function, it possible to end up with a reference to the destroyed temporary.

Example

constexpr auto &v = getOrDefault(m, key, "foo"); // Compile error!
auto &v = getOrDefault(map, key, "foo"); // Undefined behavior!

const auto value = "foo";
auto &v = getOrDefault(map, key, value); // Ok

5 Comments

If someone says auto& v = getOrDefault(m, badKey, "foo");, is it possible to end up with a reference to the destroyed temporary "foo"? Since we're returning a reference to defaultValue?
@425nesp No, this is not possible, because a compiler must not compile such case
The standard says: The requirements for constant expressions do not currently, but should, exclude expressions that have undefined behavior...link
@Childcity the question isn't only about constant expressions. constexpr can be invoked as regular functions - in that case undefined expression isn't excluded,
@КоеКто Yea, you are right! The compiler won't compile code with undefined behavior only if getOrDefault called as constant expression. The question is very old, but the c++ standard goes ahead, so my answer is for thouse, who search getOrDefault implementation in "constexpr world". Thank's for you're clarification, I will edit answer.
1

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;

6 Comments

operator[] returns an object created by invoking T(), no matter what the allocator does.
@sbi: Doesn't the map call the allocators construct method? It would be possible to change that, I think. I suspect a construct function that doesn something other than new(p) T(t); isn't well-formed, though. EDIT: In hindsight that was foolish, otherwise all the values would be the same :P Where's my coffee...
@GMan: my copy of C++03 says (in 23.3.1.2) that operator[] returns (*((insert(make_pair(x, T()))).first)).second. So unless I'm missing something, this answer is wrong.
You are right. But that seems wrong to me. Why dont they use the allocator function for inserting?
@sbi: No, I agree this answer is wrong, but for a different reason. The compiler indeed does insert with a T(), but inside insert is when it will use the allocator get memory for a new T then call construct on that memory with the parameter it was given, which is T(). So it is indeed possible to change the behavior of operator[] to have it return something else, but the allocator cannot differentiate why it's being called. So even if we made construct ignore it's parameter and use our special value, that would mean every constructed element had that value, which is bad.
|
0

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;
}

Comments

-1

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;
}

See also C++ Class wrapper around fundamental types

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.