14

The below code can be compiled successfully using Visual Studio 2015, but it failed using Visual Studio 2017. Visual Studio 2017 reports:

error C2280: “std::pair::pair(const std::pair &)”: attempting to reference a deleted function

Code

#include <unordered_map>
#include <memory>

struct Node
{
  std::unordered_map<int, std::unique_ptr<int>> map_;
  // Uncommenting the following two lines will pass Visual Studio 2017 compilation
  //Node(Node&& o) = default;
  //Node() = default;
};

int main()
{
  std::vector<Node> vec;
  Node node;
  vec.push_back(std::move(node));
  return 0;
}

It looks like Visual Studio 2017 explicit needs a move constructor declaration. What is the reason?

Hatted Rooster
  • 35,759
  • 6
  • 62
  • 122
finn
  • 205
  • 1
  • 7
  • You're missing `#include ` but as far as I can tell, that code should compile (and it does on e.g. GCC 8.2). Do you have the latest and greatest VS2017? – rubenvb Nov 06 '18 at 09:30
  • I can confirm this code fails with error code under vs2017 15.4.2. – marcinj Nov 06 '18 at 09:38
  • @rubenvb will include internally – finn Nov 07 '18 at 03:09
  • @DanielLangr, @Oliv, @Evg are all right. Looking into Vs2017's vector sourcecode, either `vec.push_back` or `vec.reserve` both call into `_Umove_if_noexcept` that internally call `_Uninitialized_copy` decided by `disjunction_v, negation>>` – finn Nov 07 '18 at 03:21
  • @finn that is entirely implementation defined and factually untrue for e.g. GCC’s libstdc++. – rubenvb Nov 07 '18 at 06:26

4 Answers4

9

Minimal example:

#include <memory>
#include <unordered_map>
#include <vector>

int main() {
  std::vector<std::unordered_map<int, std::unique_ptr<int>>> vec;
  vec.reserve(1);
}

Live demo on GodBolt: https://godbolt.org/z/VApPkH.


Another example:

std::unordered_map<int, std::unique_ptr<int>> m;
auto m2 = std::move(m);              // ok
auto m3 = std::move_if_noexcept(m);  // error C2280

UPDATE

I believe the compilation error is legal. Vector's reallocation function can transfer (contents of) elements by using std::move_if_noexcept, therefore preferring copy constructors to throwing move constructors.

In libstdc++ (GCC) / libc++ (clang), move constructor of std::unordered_map is (seemingly) noexcept. Consequently, move constructor of Node is noexcept as well, and its copy constructor is not at all involved.

On the other hand, implementation from MSVC 2017 seemingly does not specify move constructor of std::unordered_map as noexcept. Therefore, move constructor of Node is not noexcept as well, and vector's reallocation function via std::move_if_noexcept tries to invoke copy constructor of Node.

Copy constructor of Node is implicitly defined such that is invokes copy constructor of std::unordered_map. However, the latter may not be invoked here, since the value type of map (std::pair<const int, std::unique_ptr<int>> in this case) is not copyable.

Finally, if you user-define move constructor of Node, its implicitly declared copy constructor is defined as deleted. And, IIRC, deleted implicitly declared copy constructor does not participate in overload resolution. But, the deleted copy constructor is not considered by std::move_if_noexcept, therefore it will use throwing move constructor of Node.

Daniel Langr
  • 22,196
  • 3
  • 50
  • 93
  • Is this realy a bug? `std::is_copy_constructible_v>>` is true even if any attempt to copy such a type will result in a compilation error. Moreover it will also be detected as CopyInsertable, even if any try to instantiate the code to copy insert it will result in a compilation error. – Oliv Nov 06 '18 at 13:59
  • @Oliv That's a good question. It seems that the problem is that the move constructor of `std::unordered_map` is not `noexcept` in MSVC 2017 (which is seemingly not required by the Standard, or is it?). In libstdc++/libc++, it is `noexcept`. – Daniel Langr Nov 06 '18 at 14:16
  • @Oliv I updated my answer, I guess you are right. I even don't think now that it is "surprising". – Daniel Langr Nov 06 '18 at 14:25
  • @Oliv Thanks for clarificaton, I rephrased my answer not to mention _CopyInsertable_ requirement. Important is, that `std::unordered_map` defines copy constructor, just it may not be invoked if the value type cannot be copied. – Daniel Langr Nov 06 '18 at 14:44
  • I think that we have excavated a rule to apply when we will use concepts. That is to make sure that the interface of a type really reflects its requirements. For example here the copy constructor of the unordered_map should have a constraint `requires is_copy_constructible_v` or equivalent. I don't know how to name it, maybe rule *concept propagation rule* or maybe *concept explicity rule* ...?? – Oliv Nov 06 '18 at 15:57
  • Going to the standard, [`std::unordered_map`s move constructor](https://timsong-cpp.github.io/cppwp/n4659/unord.map.overview) is not declared as `noexcept`. IIUC, if libstdc++ or libc++ mark it as `noexcept`, that's a bug in libstdc++ or libc++ – Justin Nov 06 '18 at 17:49
  • @Justin In MSVC 2017, static assertion for `std::is_nothrow_move_constructible` applied to `std::unordered_map` fails: https://godbolt.org/z/Fkdflp. The very same static assertion with GCC/Clang doesn't fail. For instance, in libstdc++ the move constructor is defined as `unordered_map(unordered_map&&) = default;`. Underlying `__umap_hashtable` seems to have `noexcept` move constructor. In libstdc++ the move constructor is `noexcept` conditionally. BTW are you sure that adding `noexcept` to move constructors of Standard library types violate rules of the Standard C++? – Daniel Langr Nov 06 '18 at 18:54
  • @Oliv Concepts would be great here. However, even without concepts, it would be possible do "disable" copy constructor of a container if it's value type is not copy-constructible. Just I am not sure whether this would be Standard-compliant. – Daniel Langr Nov 06 '18 at 18:59
  • 1
    @DanielLangr I asked in the CppLang slack. It turns out that strengthening `noexcept` is compliant. – Justin Nov 06 '18 at 19:17
  • 1
    Adding `noexcept` is allowed: [\[res.on.exception.handling\] paragraph 5](https://timsong-cpp.github.io/cppwp/n4659/conforming#res.on.exception.handling-5) – Justin Nov 06 '18 at 19:26
  • It may be allowed, but it's not helpful, as it results in code that works in one standards-conforming compiler to not work in another standards-conforming compiler, thereby making the standard less useful. – Miral Jul 15 '20 at 01:38
  • @Miral The Standard effectively says that it is _unspecified_ whether the move constructor of `std::unordered_map` is or isn't `noexcept`. If you write/use a code that requires it to be `noexcept`, you restrict its usage only for implementations that satisfy this condition. BTW, it's not at all about compilers, it's a matter of Standard Library implementations. – Daniel Langr Jul 15 '20 at 06:10
6

When you declare a move constructor, the implicitly declared copy constructor is defined as deleted. On the other hand, when you don't declare a move constructor, the compiler implicitly defines the copy constructor when it need it. And this implicit definition is ill-formed.

unique_ptr is not CopyInsertable in a container that uses a standard allocator because it is not copy constructible so the copy constructor of map_ is ill-formed (it could have been declared as deleted, but this is not required by the standard).

As your example code show us, with newer version of MSVC, this ill-formed definition is generated with this example code. I do not think there is something in the standard that forbids it (even if this is realy surprising).

So you should indeed ensure that the copy constructor of Node is declared or implicitly defined as deleted.

Oliv
  • 17,610
  • 1
  • 29
  • 72
  • 1
    Im wondering as to why the issue is not seen with clang or gcc ? – darune Nov 06 '18 at 11:53
  • @darune because the generation of the code of the copy constructor is only performed when needed. So the error does not appear within this small example but it may in a larger piece of code. – Oliv Nov 06 '18 at 11:57
  • i was aware of that - somehow it *feels* like a bug in MSVC even though it might not be.. I mean if you invoke the move constructor directly (Node b(std::move(a)) ) it compiles in MSVC so why is the push_back with move (which actually turns into a emplace_back(std::move(xx))) ) not able to that when a direct move is ? possible a (cornercase) perfect forwarding issue (?) – darune Nov 06 '18 at 12:10
  • 1
    @darune I think the bug is in the standard requirement. If a standard type is not copy constructible it should be possible to check it with traits. But actually implementing such a requirement would be a real pain. I hope that with concepts this will be fixed. – Oliv Nov 06 '18 at 12:21
  • 1
    I don't have VS2017 accessible, but couldn't be the problem with that implicitly-declared move constructor of `std::unordered_map` (and therefore of `Node`) is not `noexcept`? Then, `push_back`, i.e., `emplace_back`, may prefer copy constructor (during reallocation) than potentially throwing move constructor (`std::move_if_noexcept`)? With user-declared move constructor, the copy constructor is deleted, so throwing move constructor will be used. – Daniel Langr Nov 06 '18 at 12:53
  • @DanielLangr Indeed that must be the reason why it generates the code of the ill-formed copy constructor. – Oliv Nov 06 '18 at 13:03
  • 3
    The type traits magic to do this "correctly" is surprisingly complex. I implemented an open addressing hash map (and set) that is mostly API-compatible with `unordered_map`, and ended up having to use private inheritance from `std::conditional::value && ..., AllowCopy, DisallowCopy>::type` where `AllowCopy` is empty and `DisallowCopy` has deleted copy operations (among other things). I also check for default c'tible allocator, and all of this is still not quite right, e.g. I support neither allocator propagation on copy nor copying of elements in the allocator. – Arne Vogel Nov 06 '18 at 13:32
  • 2
    `is_nothrow_move_constructible_v` is `false` in MSVC, so it tries to copy and fails; it is `true` in gcc, so it moves. If `is_nothrow_move_constructible_v` is forced to become `false` in gcc, it also tries to copy and fails. Why do MSVC and gcc disagree on `is_nothrow_move_constructible_v` value? – Evg Nov 06 '18 at 14:07
  • 2
    @Evg And `is_copy_constructible` is true even if it is not possible to copy it! It is true because `is_copy_constructible_v>` is true, while it should be required to be false by the standard. Then what happen if you check is_nothrow_copy_constructible is just chaos. – Oliv Nov 06 '18 at 14:11
5

Let's look at the std::vector source code (I replaced pointer and _Ty with actual types):

void _Umove_if_noexcept1(Node* First, Node* Last, Node* Dest, true_type)
    {   // move [First, Last) to raw Dest, using allocator
    _Uninitialized_move(First, Last, Dest, this->_Getal());
    }

void _Umove_if_noexcept1(Node* First, Node* Last, Node* Dest, false_type)
{   // copy [First, Last) to raw Dest, using allocator
    _Uninitialized_copy(First, Last, Dest, this->_Getal());
}

void _Umove_if_noexcept(Node* First, Node* Last, Node* Dest)
{   // move_if_noexcept [First, Last) to raw Dest, using allocator
    _Umove_if_noexcept1(First, Last, Dest,
        bool_constant<disjunction_v<is_nothrow_move_constructible<Node>, negation<is_copy_constructible<Node>>>>{});
}

If Node is no-throw move-constructible or is not copy-constructible, _Uninitialized_move is called, otherwise, _Uninitialized_copy is called.

The problem is that the type trait std::is_copy_constructible_v is true for Node if you do not declare a move constructor explicitly. This declaration makes copy-constructor deleted.

libstdc++ implements std::vector in a similar way, but there std::is_nothrow_move_constructible_v<Node> is true in contrast to MSVC, where it is false. So, move semantics is used and the compiler does not try to generate the copy-constructor.

But if we force is_nothrow_move_constructible_v to become false

struct Base {
    Base() = default;
    Base(const Base&) = default;
    Base(Base&&) noexcept(false) { }
};

struct Node : Base {
    std::unordered_map<int, std::unique_ptr<int>> map;
};

int main() {
    std::vector<Node> vec;
    vec.reserve(1);
}

the same error occurs:

/usr/include/c++/7/ext/new_allocator.h:136:4: error: use of deleted function ‘std::pair<_T1, _T2>::pair(const std::pair<_T1, _T2>&) [with _T1 = const int; _T2 = std::unique_ptr<int>]’
  { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
    ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Evg
  • 25,259
  • 5
  • 41
  • 83
  • 1
    Im wondering as to why the issue is not seen with clang or gcc ? – darune Nov 06 '18 at 11:53
  • 1
    @darune, the dumb answer is because gcc's and clang's implementations do not rely on `std::is_copy_constructible` in this particular piece of code. I don't have a better answer now. – Evg Nov 06 '18 at 12:14
  • If i understand you correctly that code is in the MSVC vector implementation (?) and isn't that a bug in this case ? It does feel like like a bug, because invoking the move constructor directly does compile fine. – darune Nov 06 '18 at 12:18
  • @darune, yes, the code in my answer is directly from MSVC STL implementation. I agree that it feels like a bug. – Evg Nov 06 '18 at 12:56
  • @darune, added some info about in libstdc++. – Evg Nov 06 '18 at 13:49
0

Visual Studio 2017:

As @Evg indicated, the Visual Studio 2017's vector sourcecode finally call into _Uninitialized_copy because the implicitly-declared move constructor of Node is considered as not-nothrow (is_nothrow_move_constructible<Node> is false) and is_copy_constructible<Node> is true in Visual Studio 2017.

1) About is_nothrow_move_constructible<Node>:

https://en.cppreference.com/w/cpp/language/move_constructor says:

The implicitly-declared (or defaulted on its first declaration) move constructor has an exception specification as described in dynamic exception specification (until C++17)exception specification (since C++17)

Maybe it's reasonable to consider is_nothrow_move_constructible<Node> as false beacause Node's data member std::unordered_map's move constructor is not marked as noexcept.

2) About is_copy_constructible<Node>:

As @Oliv says, it's seemingly not logical to compute is_copy_constructible<Node> as true, specially considering the fact that Node is not copy_constructible has been detected and reported as compile error by Visual Studio 2017 compiler. Node is not copy_constructible beacause std::unique_ptr is not copy_constructible.

Visual Studio 2015:

Visual Studio 2015's vector has a different implemetation. vec.push_back->_Reserve->_Reallocate->_Umove->_Uninitialized_move_al_unchecked->_Uninitialized_move_al_unchecked1->std::move(node). is_nothrow_move_constructible<Node> and is_copy_constructible<Node> are not involved. It just call std::move(node) instead of copy constructor. So the sample code can be compiled successfully using Visual Studio 2015.

finn
  • 205
  • 1
  • 7
  • The important conclusion here is IMHO that MSVC 2017 is Standard-compliant. It's move constructor of `std::unordered_map` is not `noexcept`, which is perfectly fine. It's copy constructor is defined (must be), even if it cannot be used for non-copyable _value type_. There is nothing wrong with this implementation. This is how C++ Standard is written. Hopefully, with _concepts_, we will have much more control here over corresponding stuff. – Daniel Langr Nov 08 '18 at 14:39