【问题标题】:Understanding `std::is_move_constructible`了解`std::is_move_constructible`
【发布时间】:2016-02-29 14:36:32
【问题描述】:

没有移动构造函数但具有接受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 类之间的移动可构造性之间的这种差异,请引用并在可能的情况下给出理由(例如“为禁止移动构造提供便利”-首先想到的东西)。

【问题讨论】:

  • 是的,显式声明移动构造函数为已删除显式声明它,因此它涉及重载选择,它是is_move_constructible选择的匹配项。然后,因为被删除了,所以这个失败,静态断言失败。
  • 这是迄今为止最简单的解释...
  • 他指的是我在代码中使用的static_assert——它确实失败了。

标签: c++ c++11 move-constructor


【解决方案1】:

这是对我的第一个答案的全面修改,以纠正所说的一些错误并引用标准并指出提问者希望的一些细节。

std::is_move_constructible 实际做了什么

如果T 是一个结构,那么std::is_move_constructible&lt;T&gt; 的计算结果为std::is_constructible&lt;T,T&amp;&amp;&gt;。如果T x(y) 是一些y 类型为U 的格式良好的表达式,则std::is_constructible&lt;T,U&gt; 有效。因此,要使std::is_move_constructible&lt;T&gt; 为真,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

但是,该标准允许您使用类右值初始化类左值引用。

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&amp; other) 和一个T&amp;&amp; 类型的对象y,即对T 的右值引用,那么yT&amp;T x(y) 的引用兼容是调用复制构造函数T::T(S&amp;) 的有效表达式。

示例结构的作用

让我以您的第一个示例为例,删除 const 关键字,以避免声明引用需要比初始化程序更具 cv-qualified 十次。

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

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

第二个例子做了什么

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

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

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

结论

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

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

【讨论】:

  • 这也是我的问题的重述。我已经说明了这一切。问题是,为什么?为什么移动 ctor 被删除或不存在会有所不同?
  • 您没有完全阅读详细信息:“如果未定义移动构造函数,则可以使用复制构造函数移动对象。”如果显式删除移动构造函数,则实际上定义了移动构造函数(不能使用)。让我补充最后一句。
  • 因此移动构造函数可以不存在、隐式声明和定义、隐式声明和删除、显式默认和显式定义。如果声明了重载解析,则始终选择移动构造函数(我认为在 C++14 中除外,deleted implicit move constructors are ignored)。换句话说,只有在完全不存在移动 ctor 时,复制 ctor 才会起作用,对吗?
  • Nit-pick:在 “如果T x(y) 是一个格式良好的表达式”T x(y) 不是一个表达式(它是一个声明和定义)。
  • “因此,如果您有一个复制构造函数 T::T(S&amp; other) ....”,这会导致多个细节错误;可能所有这些都不重要:如果Scv T,则构造函数T::T(S&amp;) 只是一个复制构造函数(其中cvconstvolatile 的任意组合或缺少它们)。具有引用类型的变量不是对象。值类别(左值、右值、..)仅适用于 表达式;引用声明为右值引用的变量的 id 表达式是左值表达式 (!)。引用兼容性适用于引用的类型,因此yT 是引用兼容的。
【解决方案2】:

概述

移动构造类是具有移动构造函数的类,无论是隐式的还是用户声明的。或者为右值引用调用的复制构造函数。这些构造函数将被调用,除非该类具有已删除的移动构造函数。

因此:

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

是一个移动构造类,因此你可以从各自的特征中得到真实的结果。

通过移动构造函数声明delete

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

使您的班级不可移动。

为什么会这样?

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

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

现在至于如何在编译时发生这种情况。寻找std::declval 的魔力。 std::declval 用于某处是 trait 的实现,用于评估是否可以在不实际评估表达式的情况下移动类对象。

【讨论】:

    【解决方案3】:

    在我的测试中,答案似乎又是错误的。顺便说一句,我已经在 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 的实现存在一些缺陷。


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

    【讨论】:

    • 您的代码在 GCC 6.1、7.1 和 7.2 下完美运行。您的代码中至少有一个明显的错误:缺少默认构造函数,并且可能存在包含/命名空间问题,这可能导致您得出移动不起作用的结论。试试gist.github.com/Irfy/6da0f8b11e95c9c05a12e572e1a0b4bc中的代码
    猜你喜欢
    • 2015-12-22
    • 1970-01-01
    • 1970-01-01
    • 2013-06-04
    • 2018-08-19
    • 2014-02-27
    • 2011-08-11
    • 2011-04-22
    • 1970-01-01
    相关资源
    最近更新 更多