5

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?

5
  • 3
    Since sets are a key-only your declaration of my_set is invalid. Do you mean to use std::unordered_map instead? Commented Sep 12, 2018 at 7:32
  • What about my_set[1] = MyClass(...);? This may involve a copy. If that's not intended it's a bit more complicated. Commented Sep 12, 2018 at 7:32
  • And if you mean std::unordered_map, then no there's no other way than to have a default constructor. Commented Sep 12, 2018 at 7:32
  • @Someprogrammerdude Yeah I meant std::unordered_map. Commented Sep 12, 2018 at 7:36
  • You should create a method getMyMap(key, ...) which returns my_map[key] and creates it right before using the parameters "..." for construction. Otherwise I see no other way. Moreover I would not advise to take the habit to using operator [] for maps, because if you make an error in your algorithm (retrieving a non existing key you thought existed) it will compile and run and you will not know why you get the wrong result. Commented Sep 12, 2018 at 8:19

3 Answers 3

11

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.

Sign up to request clarification or add additional context in comments.

6 Comments

"you can always do a find first" - or use try_emplace instead, if C++17 available. That would make the (in my eyes at least) so far best answer complete...
@Aconcagua You still have to have all your ctor args ready to go, which may or may not be a problem. Could be good though
@Aconcagua At the very least it's a drop-in replacement for the piecewise approach
If value-type construction is expensive, but arguments are cheap we spare the preceding lookup with find. And as the drop-in is simpler than the piece-wise stuff (well, I might now cite your own words from comment to Scheff's answer)...
Hm, hope I'm not getting pedantic, but "equivalently" is not correct in detail: While the element "may be created even if there already is[...]" with the former, it won*t be with the latter...
|
2

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)

10 Comments

@Aconcagua You're right. (I focused on the "non-default constructible" as I didn't agree with SomeProgrammerDude's comment but I wanted to check it out before realizing some wrong imagination of mine.) However, I edit the answer.
(Adjustment of previous, deleted comment): emplace might construct the MyClass element even if existing; if constructing is a cheap operation, this might be fine, if it is expensive, we might prefer something like auto i = find; auto mc = i != end ? *i : *(emplace().first);...
For these types, this seems like an overcomplicated solution. Why not just insert?
@LightnessRacesinOrbit There are many ways to get stuff into a std::map or std::unordered_map. Some of them are quite easy and other a bit more complicated but worth in special situations. The example struct T is quite simple for demonstration (and to keep the code as small as possible). emplace() has the IMHO big advance of in-place construction and hence worth to be mentioned. (May be, I should've mentioned this fact explicitly.) You might add an alternative answer showing how simple it would be with an insert(). ;-)
@Scheff I did. Generally speaking you should start with the simple solution (especially when teaching), and only get complicated when it's needed. Your code will be clearer and more maintainable that way.
|
1

[] 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.

6 Comments

insert is already "add_if_missing"; you don't need to find first
@LightnessRacesinOrbit - yes, but you don’t usually want to construct a value_type unnecessarily, which is not possible with just using the insert.
Okay - it wasn't clear that this is a constraint you meant for the hypothetical add_if_missing. Anyway in this case it seems to be of little to no cost in the grand scheme of things.
Anyway.. this, then, combined with my answer, is pretty thorough.
Nah too complicated they can stand apart :) If you want to just quickly note something about unnecessary construction you can have a +1 :P
|

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.