Does std::unordered_map operator[] do zero-initialization for non-exisiting key?
Asked Answered
L

2

34

According to cppreference.com, std::map::operator[] for non-existing value does zero-initialization.

However, the same site does not mention zero-initialization for std::unordered_map::operator[], except it does have an example which relies on this.

Of course this is just a reference site, not the standard. So, is the code below ok or not?

#include <unordered_map>
int main() {
    std::unordered_map<int, int> map;
    return map[42];     // is this guaranteed to return 0?
}
Legislative answered 5/12, 2019 at 9:44 Comment(7)
@Ælex you cannot reliably test if something is initializedErdman
@Ælex I dont really understand, how can you have a non-initialized std::optional?Erdman
@Ælex there is no way to test if an object is initialized or not because any operation on an uninitialized object other than initialization results in Undefined Behavior. A std::optional object that holds no contained value is still an initialized object.Sounding
The value object is value-initialized, not zero-initialized. For scalar types these are the same, but for class types they are different.Distributor
@Sounding I tried to test it yesterday using gnu 17 and std 17, and bizarrely all I got was zero initialisation. I thought std::optional has_value would test it but it fails, so I guess you are correct.Forgetful
@Ælex In that test, what did you do to ensure that the memory location where your std::optional was placed was in a already used memory page, and that memory location was also previously set to something else than zero? Or did you inspect the produced assembly and noticed it explicitly writes zeroes there?Legislative
@Legislative I intentionally left an uninitialised variable and tried wrapping it with std::optional and I think the compiler optimised it to zero. I did not go into the debugger, but could give it a try.Forgetful
T
21

Depending on which overload we're talking about, std::unordered_map::operator[] is equivalent to [unord.map.elem]

T& operator[](const key_type& k)
{
    return try_­emplace(k).first->second;
}

(the overload taking an rvalue-reference just moves k into try_emplace and is otherwise identical)

If an element exists under key k in the map, then try_emplace returns an iterator to that element and false. Otherwise, try_emplace inserts a new element under the key k, and returns an iterator to that and true [unord.map.modifiers]:

template <class... Args>
pair<iterator, bool> try_emplace(const key_type& k, Args&&... args);

Interesting for us is the case of there being no element yet [unord.map.modifiers]/6:

Otherwise inserts an object of type value_­type constructed with piecewise_­construct, forward_­as_­tuple(k), forward_­as_­tuple(std​::​forward<Args>(args)...)

(the overload taking an rvalue-reference just moves k into forward_­as_­tuple and, again, is otherwise identical)

Since value_type is a pair<const Key, T> [unord.map.overview]/2, this tells us that the new map element will be constructed as:

pair<const Key, T>(piecewise_­construct, forward_­as_­tuple(k), forward_­as_­tuple(std​::​forward<Args>(args)...));

Since args is empty when coming from operator[], this boils down to our new value being constructed as a member of the pair from no arguments [pairs.pair]/14 which is direct initialization [class.base.init]/7 of a value of type T using () as initializer which boils down to value initialization [dcl.init]/17.4. Value initialization of an int is zero initialization [dcl.init]/8. And zero initialization of an int naturally initializes that int to 0 [dcl.init]/6.

So yes, your code is guaranteed to return 0…

Thedathedric answered 5/12, 2019 at 10:10 Comment(1)
So good (and friendly) I'd upboat twice if I could. ThankyaForedeck
M
24

On the site you linked it says:

When the default allocator is used, this results in the key being copy constructed from key and the mapped value being value-initialized.

So the int is value-initialized:

The effects of value initialization are:

[...]

4) otherwise, the object is zero-initialized

This is why the result is 0.

Mervinmerwin answered 5/12, 2019 at 9:51 Comment(0)
T
21

Depending on which overload we're talking about, std::unordered_map::operator[] is equivalent to [unord.map.elem]

T& operator[](const key_type& k)
{
    return try_­emplace(k).first->second;
}

(the overload taking an rvalue-reference just moves k into try_emplace and is otherwise identical)

If an element exists under key k in the map, then try_emplace returns an iterator to that element and false. Otherwise, try_emplace inserts a new element under the key k, and returns an iterator to that and true [unord.map.modifiers]:

template <class... Args>
pair<iterator, bool> try_emplace(const key_type& k, Args&&... args);

Interesting for us is the case of there being no element yet [unord.map.modifiers]/6:

Otherwise inserts an object of type value_­type constructed with piecewise_­construct, forward_­as_­tuple(k), forward_­as_­tuple(std​::​forward<Args>(args)...)

(the overload taking an rvalue-reference just moves k into forward_­as_­tuple and, again, is otherwise identical)

Since value_type is a pair<const Key, T> [unord.map.overview]/2, this tells us that the new map element will be constructed as:

pair<const Key, T>(piecewise_­construct, forward_­as_­tuple(k), forward_­as_­tuple(std​::​forward<Args>(args)...));

Since args is empty when coming from operator[], this boils down to our new value being constructed as a member of the pair from no arguments [pairs.pair]/14 which is direct initialization [class.base.init]/7 of a value of type T using () as initializer which boils down to value initialization [dcl.init]/17.4. Value initialization of an int is zero initialization [dcl.init]/8. And zero initialization of an int naturally initializes that int to 0 [dcl.init]/6.

So yes, your code is guaranteed to return 0…

Thedathedric answered 5/12, 2019 at 10:10 Comment(1)
So good (and friendly) I'd upboat twice if I could. ThankyaForedeck

© 2022 - 2024 — McMap. All rights reserved.