3

I have the following problem. I have a std::unordered_map that contains an object as the value. Now I want to modify an object that I previously inserted.

class Point
{
public:
    Point(float _x, float _y) : x(_x), y(_y) {}

    float x;
    float y;
};
std::unordered_map<int, Point> points;
// ... add some values to it ...
points[1].x = 20.f; // error?

I get a weird long compile error about point not being able to be default constructed. The way I understand it operator [] returns a reference to the mapped type (aka the value), so why can't I modify it?

1
  • 2
    I think you need a default constructor for Point class so add Point() : x(0), y(0) {} so a ctor that takes no params Commented Mar 17, 2016 at 11:35

3 Answers 3

7

If the key isn't in the map, operator [] is required to create one. The expression

points[1]

needs to be able to default-insert a Point in case of lookup failure (regardless of whether lookup failure ever occurs - this is a compile-time requirement not a run-time check). That requirement cannot be satisfied by Point because Point is not default constructible. Hence the compile error. If you want to use unordered_map::operator[] , you'll need to add a default constructor.

If a default constructed Point doesn't make sense for your usage - then you simply cannot use operator[] and will have to use find throughout (or at() if you're okay with exceptions):

auto it = points.find(1);
if (it != points.end()) {
   it->second.x = 20.f;
}

points.at(1).x = 20.f; // can throw
Sign up to request clarification or add additional context in comments.

6 Comments

Or you could just use emplace to avoid the std::make_pair. You'd also need to use points.at(1) instead of points[1] for access.
The actual failure to find the point isn't what causes the error. The possibility it could fail causes the compiler to write the code where it does fail, which causes the compile-time error. This means that the next lime, points[1].x also will cause the error, despite any points.insert stuff you do first. In short, [] will not work with a type that cannot be default-constructed.
@Yakk Didn't think I was implying that it did. Better wording?
@Barry sure, but this means you didn't solve the OP's problem. The very next line uses [] again to modify the element. I'm guessing the OP tried to write a MCVE and didn't notice that an earlier part of the MCVE was generating the same error, not the line in question. In the text, the OP is clearly concerned about the points[1].x line.
@Yakk Fair point. Focused the answer and dropped the bit about insertion entirely.
|
2

operator[] constructs an object of mapped type in-place if no element exists with the given key. In a map with a default allocator, operator[] requires the mapped type to be default constructible. More generally, the mapped type must be emplace constuctible.

The easy solution is to add a default constructor to your class.

Point() : Point(0.f, 0.f) {}

If this isn't possible, you will have to use other functions to access map elements.

To access an existing mapped object, you can using at, which will throw a std::out_of_range exception if no element exists with the given key.

points.at(1).x = 20.f;

Alternatively, you can use find, which returns an iterator to the element with the given key, or to the element following the last element in the map (see end) if no such element exists.

auto it = points.find(1);
if (it != points.end())
{
    it->second = 20.f;
}

Comments

0

operator[] cannot be used on a map or unordered_map without the data being default-constructible. This is because if the object is not found, it will create it via default-construction.

The easy solution is to make your type default-constructible.

if not:

template<class M, class K, class F>
bool access_element( M& m, K const& k, F&& f ) {
  auto it = m.find(k);
  if (it == m.end())
    return false;
  std::forward<F>(f)(it->second);
  return true;
}

then:

std::unordered_map<int, Point> points;
points.emplace(1, Point(10.f, 10.f));
access_element(points, 1, [](Point& elem){
  elem.x = 20.f;
});

will do what points[1].x = 20.f; does without risking exception code or having to make Point default-constructible.

This pattern -- where we pass a function to mutate/access an element to a container -- is stealing a page from Haskell monad design. I would make it return optional<X> instead of bool, where X is the return type of the passed in function, but that is going a bit far.

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.