理解`std::is_move_constructible`

Understanding `std::is_move_constructible`

没有移动构造函数但具有接受 const T& 个参数的复制构造函数的类型,满足 std::is_move_constructible。例如,在下面的代码中:

#include <type_traits>

struct T {
    T(const T&) {}
    //T(T&&) = delete;
};

int main() {
    static_assert(std::is_move_constructible<T>::value, "not move constructible");
    return 0;
}

T 将没有隐式移动构造函数,因为它有一个用户定义的复制构造函数。

但是,如果我们取消注释移动构造函数的显式删除,代码将不再编译。为什么是这样?我原以为显式复制构造函数仍会满足 std::is_move_constructible.

重载是否起作用,选择声明的移动构造函数然后因为被删除而失败?


如果标准要求 no implicit move ctordeleted move ctor class 之间的移动可构造性之间的差异,请引用,并在可能的情况下给出理由(如 "to provide a facility for forbidding move construction"——第一个跳入脑海的事情)。

概览

移动构造函数 class 是 class 具有移动构造函数的隐式或用户声明的。或者为右值引用调用的复制构造函数。这些构造函数将被调用,除非 class 有一个删除的移动构造函数。

因此:

struct T {
    T(const T&) {}
};

移动是否可构建class,因此您从各自的特征中得到真实。

通过移动构造函数声明 delete:

struct T {
    T(const T&) {}
    T(T&&) = delete;
};

使你的 class 非移动可构造。

为什么会这样?

即使移动构造函数被删除,它也参与了重载决议,就好像它没有被删除一样。因此,首选直接匹配。

is_move_constructible提供成员常量value true 变量定义T obj(T&&);必须是良构的,如果删除T的移动构造函数是错误的-形成。因此,您得到的值等于 false。

现在说说这在编译时是如何发生的。寻找 std::declval 的魔力。 std::declval 在某处使用是特征的实现,以评估 class 对象的移动是否可以在不实际评估表达式的情况下进行。

这是对我的第一个答案的彻底修改,纠正了一些错误并引用了标准并确定了提问者希望的一些细节。

std::is_move_constructible实际做了什么

如果 T 是结构,则 std::is_move_constructible<T> 的计算结果为 std::is_constructible<T,T&&>。如果 T x(y) 是类型 U 的某些 y 的格式正确的表达式,则 std::is_constructible<T,U> 有效。因此,要使 std::is_move_constructible<T> 为真,T x(std::move(y)) 对于 T.

类型的 y 必须是格式正确的

标准引述:

The predicate condition for a template specialization is_constructible<T, Args...>
shall be satisfied if and only if the following variable definition would
be well-formed for some invented variable t:
    T t(create<Args>()...);

(...)

Template: template <class T> struct is_move_constructible;
Condition: For a referenceable type T, the same result as is_constructible<T, T&&>::value,
           otherwise false.
Precondition: T shall be a complete type, (possibly cv-qualified) void,
              or an array of unknown bound.

创建移动构造函数时

标准规定只有当用户没有声明复制构造函数、移动构造函数、赋值运算符或析构函数时才会创建默认移动构造函数。

If the definition of a class X does not explicitly declare a move
constructor, one will be implicitly declared as defaulted if and only if
—X does not have a user-declared copy constructor,
—X does not have a user-declared copy assignment operator,
—X does not have a user-declared move assignment operator, and
—X does not have a user-declared destructor

然而,标准允许您使用 class 右值初始化 class 左值引用。

Otherwise, the reference shall be an lvalue reference to a non-volatile const type
(i.e., cv1 shall be const), or the reference shall be an rvalue reference.
—If the initializer expression is an xvalue (but not a bit-field),
class prvalue, array prvalue or function lvalue and “cv1 T1”
is reference-compatible with “cv2 T2”, or (...)
then the reference is bound to the value of the initializer expression (...)
(or, in either case, to an appropriate base class subobject).

因此,如果您有一个复制构造函数 T::T(S& other) 和一个 T&& 类型的对象 y,即对 T 的右值引用,那么 yT& 引用兼容并且 T x(y) 是调用复制构造函数的有效表达式 T::T(S&).

示例结构的作用

让我来看你的第一个例子,删除 const 关键字,以避免重复十次引用需要比初始化程序更合格的 cv。

struct S {
    S(S&) {}
};

让我们检查一下情况。没有隐式默认的移动构造函数,因为有一个用户定义的复制构造函数。然而, 如果 y 属于 S 类型,则 std::move(y) 属于 S&& 类型,引用与 S& 类型兼容。因此 S x(std::move(y)) 完全有效并调用复制构造函数 S::S(const S&).

第二个例子做什么

struct T {
    T(T&) {}
    T(T&&) = delete;
};

同样,没有定义移动构造函数,因为有一个用户定义的复制构造函数一个用户定义的移动构造函数。再次让 yT 类型并考虑 T x(std::move(y)).

但是,这次表达式中可以容纳多个构造函数,因此进行了重载选择。仅尝试使用最专业的匹配构造函数,因此仅尝试调用移动构造函数 T::T(T&&)。但是移动构造函数被删除了,所以这个是无效的。

结论

第一个结构 S 可以使用其可用于执行类似移动的表达式的复制构造函数,因为它是此表达式最专用的构造函数。

第二个结构 T 必须使用其显式声明的移动构造函数来执行类似移动的表达式,同样是因为它是最专业的。然而,该构造函数被删除,移动构造表达式失败。

在我的测试中,答案似乎又错了。顺便说一下,我已经在 MSVC 17 和 GNU 6.3 上测试了以下代码:

struct T {
   T(const T&) {}
};
static_assert(is_move_constructible<T>(), "");
T a;
T b(move(a)); // compiler error here

根据此处[http://en.cppreference.com/w/cpp/language/move_constructor],用户定义的复制构造函数将禁用隐式默认移动构造函数。代码无法编译。 但奇怪的是,元函数 is_move_constructible<> 说是,编译器抱怨错误发生在位置: T(b(move(a));

因此,在我看来,is_move_constructible 的实现存在一些缺陷。


我错了,这里忘记了默认的构造函数。