Why does the emplace method in std::unordered_map take multiple parameters?

143 Views Asked by At

This was a source of confusion for me, but std::unordered_map::try_emplace takes the key and variable arguments for the constructor of the value(mapped) type, however ::emplace is supposed to take an std::pair<key, value>:

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

Ok, that is confusing. Why is it that the function signature is:

template< class... Args >
std::pair<iterator, bool> emplace( Args&&... args );

Why the multiple arguments if you're required to construct an std::pair<key, value> as described above?

3

There are 3 best solutions below

14
alfC On

In general, this is because emplace is really designed to forward the parameters to the constructor of the object. (It can be the copy constructor, in which case you use only one parameter with the value (or r-value0 you want to store, but it doesn't need to be).

In this particular example, it the value is std::pair so it is less useful.

Example:

#include<unordered_map>

int main() {
    std::unordered_map<int, double> m;
    m.emplace(42, 3.14);  // the constructor of pair will be called with these two parameters.
}

https://godbolt.org/z/PYx51jT4x

Some people do not recommend using emplace for some containers because there is really not much gain and it can be misleading, because many times the value will be constructed anyway before knowing "where" in needs to be and then copied there, instead of really emplaced.

Take a look at Meyer's book, "Effective Modern C++: 42 Specific Ways to Improve Your Use of C++11 and C++14"

Not sure now if unordered_set is one of these.

0
Jan Schultke On

std::unordered_map::emplace perfectly forwards all arguments to the constructor of std::pair, so it's as if you had called:

// value_type is defined as std::pair<...> for a std::unordered_map
value_type(std::forward<Args>(args...)

In general, .emplace forwards all arguments to the value_type of standard library containers.

For std::unordered_map, this opens up countless possibilities, because now you can:

  • call unary constructors, like .emplace(std::pair{key, value})
  • call binary constructors, like .emplace(key, value)
  • call other constructors, like .emplace(std::piecewise_construct, std::forward_as_tuple(key_args...), std::forward_as_tuple(value_args...))

std::unordered_map::try_emplace is different because it constructs a key separately from the value. It only constructs the full std::pair which is stored in the map if the key doesn't exist yet. Obviously, it cannot simply dump all arguments into a std::pair and requires some separation.


Note: for most intents and purposes, try_emplace is preferred over emplace. See also this answer.

Note: everything said here equally applies to std::map.

0
user17732522 On

For all containers emplace takes the arguments and forwards them to the constructor of the value_type, which is an std::pair of the const key and mapped type for a std::unordered_map.

std::pair has constructors that take 0, 1, 2, and 3 arguments. So emplace should support all of them. It is true that there are no std::pair constructors that allow more than 3 arguments specified in the standard library, but there wouldn't be much gained specifying this particular emplace to accept only up to 3 arguments.

Also, a user is allowed to specialize std::pair for their own types (as long as it satisfies the standard library requirements) and may add a constructor that takes more than 3 arguments. In that case emplace should not unnecessarily be imcompatible.

Here is an example where the only possible way to insert elements into the map is with three arguments to emplace, because the key type is non-movable, using the std::piecewise_construct constructor of std::pair which avoids all copies/moves of the key or mapped type.

struct Key {
    int i;
    Key(int i) : i(i) {}
    Key(const Key&) = delete;
    Key& operator=(const Key&) = delete;
    bool operator==(const Key&) const = default;
};

constexpr auto hash = [](const Key& k){ return k.i; };

int main() {
    std::unordered_map<Key, int, decltype(hash)> m;
    m.emplace(std::piecewise_construct, std::forward_as_tuple(1), std::forward_as_tuple(1));
}