C++ Map Initialize object using non-default constructor

Viewed 5977

In C++ suppose I have an unordered map defined as follows:

unordered_map<int, MyClass> my_map;
auto my_class = my_map[1];

In the above code if 1 is not present as key in my_map it will initialize MyClass with default constructor and return. But is there a way to use non-default constructor of MyClass for initialization?

3 Answers

You're right that operator[] needs the value type to be default-constructible.

insert does not:

std::unordered_map<int, MyClass> my_map;
// Populate the map here

// Get element with key "1", creating a new one
// from the given value if it doesn't already exist
auto result = my_map.insert({1, <your value here>});

This gives you a pair containing an iterator to the element (whether created new, or already present), and a boolean (telling you which was the case).

So:

auto& my_class = *result.first;
const bool was_inserted = result.second;

Now you can do whatever you like with this information. Often you won't even care about result.second and can just ignore it.

For more complex value types you can play around with emplace, which is like insert but, um, better. Say you really don't want the value to be constructed if it won't be used, and you have C++17:

auto result = my_map.try_emplace(1, <your value's ctor args here here>);

If you don't care (or don't have C++17):

auto result = my_map.emplace(1, <your value>);

This is still better than insert as it can move the value into the map, rather than copying it.

Ultimately, and if you don't even want to unnecessarily produce your ctor args, you can always just do a find first, but it's nice to try to avoid that, as the insertion operation itself will be doing a find too.

Imagine a struct T:

struct T {
  int i1, i2;
  // no default constructor
  explicit T(int i1, int i2): i1(i1), i2(i2) { }
};

With a default constructor it's quite easy:

aMap[123] = T(1, 23);

The operator[] grants that a non-existing entry is created on demand (but for this it needs the default constructor of the mapped type).


If the class of mapped_type doesn't provide a default constructor OP's intention can be matched by a simple combination of std::unordered_map::find() and std::unordered_map::insert() (or just only the latter with check of success).

(This part was inserted later as A Lightness Races in Orbit pointed out that I skipped this simple solution and directly moved to the more complicated.) He wrote an alternative answer concerning this. As it is lacking a demonstrational MCVE, I took mine and adapted it:

#include <iostream>
#include <unordered_map>

struct T {
  int i1, i2;
  // no default constructor
  explicit T(int i1, int i2): i1(i1), i2(i2)
  {
    std::cout << "T::T(" << i1 << ", " << i2 << ")\n";
  }
};

int main()
{
  typedef std::unordered_map<int, T> Map;
  Map aMap;
  //aMap[123] = T(1, 23); doesn't work without default constructor.
  for (int i = 0; i < 2; ++i) {
    Map::key_type key = 123;
    Map::iterator iter = aMap.find(key);
    if (iter == aMap.end()) {
      std::pair<Map::iterator, bool> ret
        = aMap.insert(Map::value_type(key, T(1 + i, 23)));
      if (ret.second) std::cout << "Insertion done.\n";
      else std::cout << "Insertion failed! Key " << key << " already there.\n";
    } else {
      std::cout << "Key " << key << " found.\n";
    }
  }
  for (const auto &entry : aMap) {
    std::cout << entry.first << " -> (" << entry.second.i1 << ", " << entry.second.i2 << ")\n";
  }
  return 0;
}

Output:

T::T(1, 23)
Insertion done.
Key 123 found.
123 -> (1, 23)

Live Demo on coliru


If the mapped type does lack a copy constructor as well then it's still solvable using std::unordered_map::emplace() (again with or without pre-check with std::unordered_map::find()):

aMap.emplace(std::piecewise_construct,
  std::forward_as_tuple(123),
  std::forward_as_tuple(1, 23));

The adapted sample:

#include <iostream>
#include <unordered_map>

struct T {
  int i1, i2;
  // no default constructor
  explicit T(int i1, int i2): i1(i1), i2(i2)
  {
    std::cout << "T::T(" << i1 << ", " << i2 << ")\n";
  }
  // copy constructor and copy assignment disabled
  T(const T&) = delete;
  T& operator=(const T&);
};

int main()
{
  typedef std::unordered_map<int, T> Map;
  Map aMap;
  for (int i = 0; i < 2; ++i) {
    Map::key_type key = 123;
    Map::iterator iter = aMap.find(key);
    if (iter == aMap.end()) {
      std::pair<Map::iterator, bool> ret
        = aMap.emplace(std::piecewise_construct,
          std::forward_as_tuple(key),
          std::forward_as_tuple(1 + i, 23));
      if (ret.second) std::cout << "Insertion done.\n";
      else std::cout << "Insertion failed! Key " << key << " already there.\n";
    } else {
      std::cout << "Key " << key << " found.\n";
    }
  }
  for (const auto &entry : aMap) {
    std::cout << entry.first << " -> (" << entry.second.i1 << ", " << entry.second.i2 << ")\n";
  }
  return 0;
}

Output:

T::T(1, 23)
Insertion done.
Key 123 found.
123 -> (1, 23)

Live Demo on coliru

As Aconcagua mentioned in comment, without the pre-checking find(), the emplace() might construct the mapped value even if the insertion will fail.

The doc. of `std::unordered_map::emplace() on cppreference mentions this:

The element may be constructed even if there already is an element with the key in the container, in which case the newly constructed element will be destroyed immediately.


As Jarod42 mentioned, std::unordered_map::try_emplace() is an alternative in C++17 worth to be mentioned as

Unlike insert or emplace, these functions do not move from rvalue arguments if the insertion does not happen, which makes it easy to manipulate maps whose values are move-only types, such as std::unordered_map<std::string, std::unique_ptr<foo>>. In addition, try_emplace treats the key and the arguments to the mapped_type separately, unlike emplace, which requires the arguments to construct a value_type (that is, a std::pair)

[] implements get_add_if_missing. Semantically, an overhead-free implementation would be something like:

value_type& get_add_if_missing(key_type const& k, auto&& factory) {
    auto b = bucket_for(k);
    auto pos = pos_for(k, b);
    if (pos == b.end()) {
        return b.append(k, factory());
    } else {
        return *pos;
    }
}

A full equivalent is not there on the API yet (as of C++17), so for now, you need to decide what suboptimality to have based on how expensive it is to creating a temporary value_type:

  • do an extra lookup (search then insert if missing)
  • extra temporary (insert/emplace always, covered well in other answers)

An extra lookup version is:

final itr = m.find(key);
if (itr == m.end()) {
    // insert or emplace a new element with the constructor of your choice
}

The std::unordered_map article on cppreference should have enough usage examples for insert / emplace.

With a tree-based implementation (std::map) a zero overhead get_add_if_missing emulation is quite possible with lower_bound followed by a hint-enabled insert / emplace.

And finally the good news -- if you can accept Boost.Intrusive (a header-only library) as a dependency, you can build a truly zero-overhead get_add_if_missing (no temporaries or repeated hash calculation). An API of the hash map from there is sufficiently detailed for that.

Related