17

Have a look at the following code:

#include <utility>
#include <map>

// non-copyable but movable
struct non_copyable {
    non_copyable() = default;

    non_copyable(non_copyable&&) = default;
    non_copyable& operator=(non_copyable&&) = default;

    // you shall not copy
    non_copyable(const non_copyable&) = delete;
    non_copyable& operator=(const non_copyable&) = delete;
};

int main() {
    std::map<int, non_copyable> map;
    //map.insert({ 1, non_copyable() });  < FAILS
    map.insert(std::make_pair(1, non_copyable()));
    // ^ same and works
}

Compiling this snippet fails when uncommenting the marked line on g++ 4.7. The error produced indicates that non_copyable can't be copied, but I expected it to be moved.

Why does inserting a std::pair constructed using uniform initialization fail but not one constructed using std::make_pair? Aren't both supposed to produce rvalues which can be successfully moved into the map?

mfontanini
  • 21,410
  • 4
  • 65
  • 73

2 Answers2

22

[This is a complete rewrite. My earlier answer had nothing to do with the problem.]

The map has two relevant insert overloads:

  • insert(const value_type& value), and

  • <template typename P> insert(P&& value).

When you use the simple list-initializer map.insert({1, non_copyable()});, all possible overloads are considered. But only the first one (the one taking const value_type&) is found, since the other doesn't make sense (there's no way to magically guess that you meant to create a pair). The first over­load doesn't work of course since your element isn't copyable.

You can make the second overload work by creating the pair explicitly, either with make_pair, as you already described, or by naming the value type explicitly:

typedef std::map<int, non_copyable> map_type;

map_type m;
m.insert(map_type::value_type({1, non_copyable()}));

Now the list-initializer knows to look for map_type::value_type constructors, finds the relevant mova­ble one, and the result is an rvalue pair which binds to the P&&-overload of the insert function.

(Another option is to use emplace() with piecewise_construct and forward_as_tuple, though that would get a lot more verbose.)

I suppose the moral here is that list-initializers look for viable overloads – but they have to know what to look for!

Kerrek SB
  • 464,522
  • 92
  • 875
  • 1,084
  • 1
    Yes, that's basically what I wrote before deleting my answer. I have a doubt though: why is an `initializer_list<>` created here? `std::pair` does not seem to have a constructor that takes one. I thought the uniform initialization syntax would just pick the regular constructor of `pair<>`. – Andy Prowl Feb 17 '13 at 00:11
  • 1
    Also, initializer_list<> elements must be all of the same type, no? – eladidan Feb 17 '13 at 00:21
  • +1 to both comments. There shouldn't even be an `initializer_list` in my example. It's more like a call to `std::pair`'s constructor using uniform initialization. – mfontanini Feb 17 '13 at 00:23
  • 7
    I suppose what's surprising is that `map` does *not* have an `insert` overload for `value_type &&`, while `vector` does. – Kerrek SB Feb 17 '13 at 01:26
  • 1
    @KerrekSB C++17 added one, BTW – n.caillou Oct 03 '17 at 23:29
  • @n.caillou: Thanks, four-years-from-the-future person :-) – Kerrek SB Oct 03 '17 at 23:39
  • I nevertheless appear to be programming six years ago, since I ran into this very problem! – n.caillou Oct 04 '17 at 00:03
  • Is there any solution for C++03? I am programming 10 years ago :'-( – Fabian Dec 16 '19 at 09:32
  • @Fabian: You mean you have a non-copyable type in C++03? I don't think a std::map will work with that directly, though you could build your own "fork" that has a dedicated "emplace-like" insertion function for your specific data type, if you really need this feature. Or you could use more indirection and store pointers, but that wouldn't quite be comparable. – Kerrek SB Dec 17 '19 at 01:08
0

Besides other answers of providing move (assignment) constructor, you could also store the non-copyable object through pointer, especially unique_ptr. unique_ptr will handle resource movement for you.

  • This is confusing answer. unique_ptr will not handle resource movement of the object, but rather you are storing only the pointer to the object itself, therefore no resources of the object are moved - only the unique_ptr itself is moved. – Petr Jan 20 '20 at 13:45