为什么隐式和显式删除的移动构造函数会受到不同对待?

Why are implicitly and explicitly deleted move constructors treated differently?

C++11标准中隐式和显式删除移动构造函数的不同处理背后的基本原理是什么,尊重containing/inheriting classes?

的移动构造函数的隐式生成

C++14/C++17 有什么改变吗? (C++14 中的 DR1402 除外)

注意:我明白发生了什么,我明白这是根据 C++11 标准的规则,我对这些暗示这种行为的规则的基本原理感兴趣(请确保不要简单地重申它就是这样,因为标准是这样说的)。


假设一个 class ExplicitDelete 有一个显式删除的移动构造函数和一个显式默认的复制构造函数。这个 class 不是 move constructible 即使 compatible copy ctor 可用,因为重载解析选择移动构造函数并且由于它的删除而在编译时失败。

假设一个 class ImplicitDelete 包含或继承自 ExplicitDelete 并且什么都不做。由于 C++11 移动构造函数规则,此 class 将其移动构造函数隐式声明为已删除。然而,这个 class 仍然是 move constructible 通过它的复制构造函数。 (最后这句话与 DR1402 的分辨率有关吗?)

然后 class Implicit containing/inheriting 来自 ImplicitDelete 将生成一个完美的隐式移动构造函数,它调用 ImplicitDelete 的复制构造函数。

那么允许 Implicit 能够隐式移动而 ImplicitDelete 不能隐式移动背后的基本原理是什么?

在实践中,如果 ImplicitImplicitDelete 有一些重型可移动部件(想想 vector<string>),我认为 Implicit 没有理由要优越得多ImplicitDelete 的移动性能。 ImplicitDelete 仍然可以从其隐式移动构造函数复制 ExplicitDelete——就像 ImplicitImplicitDelete.

所做的那样

对我来说,这种行为似乎不一致。如果发生这两件事中的任何一件,我会发现它更一致:

  1. 编译器对隐式和显式删除的移动构造函数一视同仁:

    • ImplicitDelete 变得不是 move-constructible,就像 ExplicitDelete
    • ImplicitDelete 的已删除移动构造函数导致 Implicit 中已删除的隐式移动构造函数(与 ExplicitDeleteImplicitDelete 的相同方式)
    • Implicit 变成不move-constructible
    • 在我的代码示例中编译 std::move 行完全失败
  2. 或者,编译器返回复制 ctor also for ExplicitDelete:

    • ExplicitDelete 的复制构造函数在所有 move 中被调用,就像 ImplicitDelete
    • ImplicitDelete 得到一个正确的隐式移动 ctor
    • Implicit在这种情况下没有变化)
    • 代码示例的输出表明 Explicit 成员始终被移动。

这是完整的示例:

#include <utility>
#include <iostream>
using namespace std;

struct Explicit {
    // prints whether the containing class's move or copy constructor was called
    // in practice this would be the expensive vector<string>
    string owner;
    Explicit(string owner) : owner(owner) {};
    Explicit(const Explicit& o) { cout << o.owner << " is actually copying\n"; }
    Explicit(Explicit&& o) noexcept { cout << o.owner << " is moving\n"; }
};
struct ExplicitDelete {
    ExplicitDelete() = default;
    ExplicitDelete(const ExplicitDelete&) = default;
    ExplicitDelete(ExplicitDelete&&) noexcept = delete;
};
struct ImplicitDelete : ExplicitDelete {
    Explicit exp{"ImplicitDelete"};
};
struct Implicit : ImplicitDelete {
    Explicit exp{"Implicit"};
};

int main() {
    ImplicitDelete id1;
    ImplicitDelete id2(move(id1)); // expect copy call
    Implicit i1;
    Implicit i2(move(i1)); // expect 1x ImplicitDelete's copy and 1x Implicit's move
    return 0;
}

So what is the rationale behind allowing Implicit to be able to move implicitly and ImplicitDelete not to be able to move implicitly?

理由是这样的:你描述的情况没有意义。

看,这一切都是因为ExplicitDelete才开始的。根据您的定义,此 class 有一个显式删除的移动构造函数,但有一个默认的复制构造函数。

有固定类型,既没有复制也没有移动。有仅移动类型。还有可复制的类型。

但是一个可以复制但有一个显式删除移动构造函数的类型?我会说这样的class是矛盾的

以下是我所看到的三个事实:

  1. 明确删除移动构造函数应该意味着您不能移动它。

  2. 显式默认复制构造函数应该意味着您可以复制它(当然是为了本次对话的目的。我知道您仍然可以做一些事情来删除显式默认值)。

  3. 类型可以复制,就可以移动。这就是为什么存在关于不参与重载决策的隐式删除移动构造函数的规则。因此,移动是复制的真子集。

C++ 在此实例中的行为是不一致的,因为您的代码是矛盾的。您希望您的类型可复制但不可移动; C++ 不允许这样做,所以它的行为很奇怪。

看看当你消除矛盾时会发生什么。如果你显式地删除 ExplicitDelete 中的复制构造函数,一切又变得有意义了。 ImplicitDelete 的 copy/move 构造函数被隐式删除,因此它是固定的。 Implicit 的 copy/move 构造函数被隐式删除,因此它也是固定的。

如果您编写自相矛盾的代码,C++ 将不会以完全合法的方式运行。