为什么隐式和显式删除的move构造函数被区别对待

Why are implicitly and explicitly deleted move constructors treated differently?

本文关键字:move 构造函数 区别对待 删除 为什么      更新时间:2023-10-16

对于包含/继承类的移动构造函数的隐式生成,C++11标准中隐式和显式删除的移动构造函数不同处理背后的原理是什么?

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

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


假设一个类ExplicitDelete具有一个明确删除的move ctor和一个明确默认的copy ctor。即使有兼容的复制构造函数可用,这个类也不是move constructible,因为重载解析选择了move构造函数,并且在编译时由于其删除而失败。

假设一个类ImplicitDelete包含ExplicitDelete或从CCD_4继承而不做任何其他事情。由于C++11的move ctor规则,该类将隐式声明其move ctors为已删除。但是,这个类通过它的复制ctor仍然是move constructible。(最后一句话与DR1402的分辨率有关吗?)

然后,包含/继承ImplicitDelete的类Implicit将生成一个非常精细的隐式移动构造函数,该构造函数调用ImplicitDelete的复制构造函数。

那么,允许Implicit能够隐式移动而ImplicitDelete不能隐式移动的理由是什么呢?

在实践中,如果ImplicitImplicitDelete有一些重型可移动构件(想想vector<string>),我认为Implicit在移动性能上没有理由大大优于ImplicitDelete。CCD_ 16仍然可以从其隐式移动ctor—就像CCD_ 18对CCD_。


在我看来,这种行为似乎前后矛盾。如果这两件事中的任何一件发生,我会发现它更一致:

  1. 编译器对隐式和显式删除的移动因子一视同仁:

    • ImplicitDelete变成了move-constructible,就像ExplicitDelete一样
    • ImplicitDelete的已删除移动ctor导致Implicit中的已删除隐式移动ctor(与ExplicitDeleteImplicitDelete所做的相同)
    • Implicit变为非move-constructible
    • 在我的代码示例中,std::move行的编译完全失败
  2. 或者,编译器返回到ExplicitDelete:的复制ctor

    • ExplicitDelete的复制构造函数在所有move中都被调用,就像ImplicitDelete一样
    • ImplicitDelete得到一个适当的隐式移动因子
    • 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 copyingn"; }
    Explicit(Explicit&& o) noexcept { cout << o.owner << " is movingn"; }
};
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;
}

那么,允许隐式移动和隐式删除不能隐式移动背后的理由是什么呢?

理由是:你所描述的案例没有意义。

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

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

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

在我看来,以下是三个事实:

  1. 明确删除移动构造函数意味着你不能移动它

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

  3. 如果一个类型可以被复制,那么它就可以被移动。这就是为什么存在关于隐式删除的移动构造函数不参与重载解析的规则。因此,移动是复制的一个子集。

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

看看当你消除矛盾时会发生什么。如果显式删除ExplicitDelete中的复制构造函数,那么一切都有意义了。ImplicitDelete的复制/移动构造函数被隐式删除,因此它是不可移动的。Implicit的复制/移动构造函数被隐式删除,因此它也是不可移动的。

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