Why explicit-ness of std::pair's heterogeneous "move"-constructor changed in C++17?

Viewed 180

Pardon the convoluted title, and consider this code:

#include <memory>
#include <utility>

using X = std::unique_ptr<int>;

using A = std::pair<int, const X>;
using B = std::pair<int,       X>;

static_assert(std::is_constructible<A, B>::value, "(1)"); // Ok.
static_assert(std::is_convertible<B, A>::value, "(2)"); // Rejected by GCC and Clang, in C++14 and before.

static_assert(std::is_constructible<const X, X>::value, "(3)"); // Ok.
static_assert(std::is_convertible<X, const X>::value, "(4)"); // Ok.

In short, the constructor of std::pair<int, const std::unique_ptr<int>> from an rvalue of type std::pair<int, std::unique_ptr<int>> is explicit in C++14, and implicit and C++17 and newer. Where does this difference come from?

Only libstdc++ and libc++ demonstrate this behavior. In MSVC's library, the constructor is always implicit.

Why was it explicit in C++14? I don't see anything relevant on cppreference (constructor (6)).

1 Answers

The relevant constructors have not been explicit before C++17 either.

GCC and Clang in pre-C++17 mode are actually considering std::is_convertible<B, A>::value false because A's move constructor is implicitly deleted.

The move constructor is implicitly deleted, because a const std::unique_ptr cannot be moved or copied.

The move constructor is required to fulfill std::is_convertible<B, A>::value before C++17, because it tests whether a function of the form

A test() {
    return std::declval<B>();
}

would be well-formed. The return statement copy-initializes the A return value from the operand and before C++17 copy-initialization always involved a conversion to a temporary of the target type if necessary, then followed by direct-initialization of the target from the temporary. That direct-initialization would use the move constructor of A. The move can be elided by the compiler, but the move constructor must still be usable.

Since C++17 mandatory copy elision applies and even conceptually the copy-initialization does not contain the direct-initialization via the move constructor anymore.

Related