3

Code

#include <iostream>
#include <unordered_map>
#include <utility>

using namespace std;

struct Foo {
    Foo(const int value) : val(value) {
        cout << "Foo(int), val: " << val << '\n';
    }
    Foo(Foo & foo) {
        val = foo.val;
        cout << "Foo(Foo &)" << '\n';
    }
    Foo(const Foo & foo) {
        val = foo.val;
        cout << "Foo(const Foo &)" << '\n';
    }
    Foo(Foo && foo) {
        val = foo.val;
        cout << "Foo(Foo &&)" << '\n';
    }
    ~Foo() { cout << "~Foo(), val: " << val << '\n'; }
    Foo& operator=(const Foo& rhs)
    {
        cout << "Foo& operator=(const Foo& rhs), rhs.val: " << rhs.val;
        val = rhs.val;
        return *this;
    }
    bool operator==(const Foo& rhs) const { return val == rhs.val; }
    bool operator<(const Foo& rhs)  const { return val < rhs.val; }

    int val;
};
template<> struct std::hash<Foo> {
    size_t operator()(const Foo& f) const { return hash<int>{}(f.val); }
};

int main()
{
    std::unordered_map<Foo, int> mp;
    mp.insert(std::pair<Foo, int>{1, 50});
    std::cout << '\n';
    mp.insert(std::pair<const Foo, int>{2, 60});
    std::cout << '\n';

    std::cout << "exiting main()\n";
}

Output

Foo(int), val: 1
Foo(Foo &&)
~Foo(), val: 1

Foo(int), val: 2
Foo(const Foo &)
~Foo(), val: 2

exiting main()
~Foo(), val: 1
~Foo(), val: 2

Question #1

Why does mp.insert(std::pair<Foo, int>{1, 50}) compile? Does an implicit conversion occur?

Let's break it down as I see it.

  1. Foo(int) with val 1 is called when temporary std::pair is created, val 1 is provided to Foo ctor. Foo(int), val: 1 is printed.
  2. std::pair<iterator, bool> insert(value_type && value) is called with temporary pair provided as argument. How? The std::unordered_map<Foo, int>::value_type is std::pair<const Foo, int>. Does an implicit conversion from std::unordered_map<Foo, int> to std::unordered_map<const Foo, int> occur here? If so, should the copy ctor be invoked here?
  3. key is searched in the map. The key is not found, so the node is allocated and initialized with std::pair{std::move(value)}; ctor. Foo(Foo &&) is printed.
  4. Temporary pair object is destructed. ~Foo(), val: 1 is printed.

Question #2

Why Foo's move ctor is called on the first insert and Foo's copy ctor is called on the second insert?

5
  • Question #1 [,,,] Does an implicit conversion occur? Yes. For question 2 std::pair<const Foo, int>{2, 60} has a const Foo. const objects cannot be moved as moving modifies an object. Commented Apr 4, 2024 at 12:53
  • "Yes". Then what about this part: "If so, should the copy ctor be invoked here?" ? Furthermore, if an implicit conversion to const Foo occurs in the first case, then why move ctor on const Foo object is called? Commented Apr 4, 2024 at 13:00
  • The source is not const so it is perfectly fine to move construct a const object from a non-const object. Commented Apr 4, 2024 at 13:05
  • As I see it, the Foo's move ctor is called in step 3 (inside the insert(value_type && value) scope), when node is initialized with std::move(value). Not when an implicit conversion is made. Commented Apr 4, 2024 at 13:08
  • Yes. The inner node gets constructed from move(val). Commented Apr 4, 2024 at 13:09

2 Answers 2

3

Answer to Question 1

An implicit conversion is performed, but not in the way you expect it. A different overload for the insert() method is used that itself performs the conversion. To be more precise:

  1. Correct
  2. Incorrect. Instead the overload template<class P> iterator insert(P && value); is called, since the pair is not of the type const value_type & and not value_type &&, or of node_type &&, which are the other possible single-argument overloads here. See the C++ reference for unordered_map::insert() for all overloads.
  3. In essence yes.
  4. Yes.

Answer to Question 2

The problem is that a move of std::pair<const Foo, int> is only possible if Foo defines a const rvalue move constructor, i.e. Foo(const Foo && other). Since that specific constructor does not exist, the compiler must resort back to the copy constructor of the pair, that's why that one is called.

(Not to say you should define const rvalue move constructors. Unless you absolutely need them in very specific circumstances, I'd avoid them because it makes reasoning about your class more difficult by adding additional complexity.)

General Note

You might also want to take a look at unordered_map::emplace if you want to avoid needless constructor calls.

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

1 Comment

Thanks! This perfectly explains everything.
0

Thanks for the interesting case! You have found non-optimal implementation of the method std::pair<iterator, bool> insert(value_type && value) Of course there should be no copying for temporary objects (I checked for clang++, g++ and msvc). Since such an overload was added to the unordered_map interface, its implementation should be like this:

auto insert(value_type&& pair)
{
    return map.emplace(const_cast<Key&&>(pair.first), pair.second);
}

Here is a training class.

template<typename Key, typename T> class TMyMap
{
private:
    std::unordered_map<Key, T> map;
public:
    using value_type = std::pair<const Key, T>;
    auto insert(value_type&& pair)
    {
        std::cout << "auto insert(value_type&& pair)\n";
        return map.emplace(const_cast<Key&&>(pair.first), pair.second);
    }
    template<typename P> auto insert(P&& pair)
    {
        std::cout << "template<typename P> auto insert(P&& p)\n";
        return map.insert(std::forward<P>(pair));
    }
};

You can use it for your example.

And you were given the wrong answer: «The problem is that a move of std::pair<const Foo, int> is only possible if Foo defines a const rvalue move constructor, i.e. Foo(const Foo && other). Since that specific constructor does not exist, the compiler must resort back to the copy constructor of the pair, that's why that one is called». A const rvalue move constructor is nonsense. For constant objects it is not possible to use move-semantics (or rather, it is possible, but only in a very distorted form - only when all members of the class are defined as mutable).

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.