【问题标题】:Move constructor is required even if it is not used. Why?即使不使用移动构造函数也是必需的。为什么?
【发布时间】:2014-07-10 14:05:52
【问题描述】:

为什么?!为什么 C++ 要求类是可移动的,即使它没有被使用! 例如:

#include <iostream>
using namespace std;

struct A {
    const int idx;
    //   It could not be compileld if I comment out the next line and uncomment
    // the line after the next but the moving constructor is NOT called anyway!
    A(A&& a) : idx(a.idx) { cout<<"Moving constructor with idx="<<idx<<endl; }
   //  A(A&& a) = delete;
    A(const int i) : idx(i) { cout<<"Constructor with idx="<<i<<endl; }
    ~A() { cout<<"Destructor with idx="<<idx<<endl; }
};

int main()
{
    A a[2] = { 0, 1 };
   return 0;
}

输出是(没有调用移动构造函数!):

idx=0 的构造函数
idx=1 的构造函数
idx=1 的析构函数
idx=0 的析构函数

如果移动构造函数被删除,则代码无法编译('使用已删除的函数'A::A(A&&)''。但如果构造函数没有被删除,它就不会被使用! 多么愚蠢的限制? 笔记: 为什么我需要它?当我尝试初始化包含 unique_ptr 字段的对象数组时,就会出现实际意义。 例如:

// The array of this class can not be initialized!
class B {
    unique_ptr<int> ref;
public:
    B(int* ptr) : ref(ptr)
        {  }
}
// The next class can not be even compiled!
class C {
    B arrayOfB[2] = { NULL, NULL };
}

如果您尝试使用 unique_ptr 的向量,情况会变得更糟。

好的。非常感谢大家。所有那些复制/移动构造函数和数组初始化都有很大的混淆。实际上,问题是关于编译器需要复制构造器,可能使用移动构造器并且不使用它们的情况。 因此,稍后当我获得普通键盘时,我将创建一个新问题。我会在这里提供链接。

附注我已经提出了更具体和明确的问题 - 欢迎来到discuss它!

【问题讨论】:

  • C++ 不要求类在不移动的情况下是可移动的。你跳到了错误的结论。
  • 不不不...它需要一个移动的构造函数,但它不使用它。所以类没有移动,但需要构造函数。我的结论有什么问题?
  • 不不不,你错了。
  • 好的。让我们一步一步来。 1) 班级搬家了吗? 2) 是否需要移动构造函数?

标签: c++ c++11 std c++14


【解决方案1】:
A a[2] = { 0, 1 };

从概念上讲,这会创建两个临时的A 对象A(0)A(1),并移动或复制它们以初始化数组a;所以需要一个移动或复制构造函数。

作为优化,允许省略移动或复制,这就是您的程序似乎没有使用移动构造函数的原因。但是仍然必须有一个合适的构造函数,即使它的使用被省略了。

【讨论】:

  • 我怎么知道什么时候没有调用构造函数呢?
  • @user3544995 你不需要关心。您的复制/移动构造函数不应该有任何额外的副作用,但正如您所展示的,您可以引入一个副作用(例如使用 cout 打印)只是为了查看它是否被调用。
  • @user3544995:通常情况下,您不会在意 - 无论移动是否被省略,程序的行为都会相同,而省略将是纯粹的优化。只有在(如此处)它执行的操作不是简单地移动对象时,您才会注意到差异 - 这将是一件奇怪的事情。
  • @user3544995 因为复制/移动省略是可选的优化。事实上,只要不影响程序的执行,编译器就可以省略任何内容(这称为as-if 规则)。明确提到复制/移动构造函数的唯一原因是,如果复制/移动构造函数有副作用(这会影响程序的执行),它甚至可能发生。
  • @user3544995:“如果记录了移动构造函数没有被调用,为什么它需要存在?” - 不是,编译器可以选择是否调用它。它必须存在,否则代码的有效性将取决于编译器的优化选择。
【解决方案2】:
A a[2] = { 0, 1 };

这是聚合初始化。 §8.5.1 [dcl.init.aggr]/p2 标准规定

当聚合由初始化列表初始化时,如 8.5.4 中所指定,初始化列表的元素被视为聚合成员的初始化,按递增的下标或成员顺序。每个成员都是从相应的initializer-clause复制初始化

§8.5 [dcl.init]/p16 依次描述了类类型对象复制初始化的语义:

  • [...]
  • 如果目标类型是(可能是 cv 限定的)类类型:
    • 如果初始化是直接初始化,或者如果是复制初始化,其中源的 cv 不合格版本 type 与该类的类相同或派生类 目的地,构造函数被考虑。适用的构造函数 枚举(13.3.1.3),通过重载选择最好的 决议 (13.3)。调用如此选择的构造函数进行初始化 对象,以初始化表达式或表达式列表作为其 论点。如果没有构造函数适用,或者重载决议是 模棱两可,初始化格式不正确。
    • 否则(即,对于剩余的复制初始化情况),可以从源转换的用户定义转换序列 类型为目标类型或(使用转换函数时) 其派生类的枚举如 13.3.1.4 中所述, 最好的一个是通过重载决议(13.3)选择的。如果 转换无法完成或不明确,初始化是 格式不正确。使用初始化程序调用所选函数 表达式作为它的参数;如果函数是构造函数,则调用 初始化 cv 不合格版本的临时版本 目的地类型。临时是prvalue。调用结果 (这是构造函数的临时情况)然后用于 直接初始化,根据上面的规则,对象是 复制初始化的目的地。在某些情况下,一个 允许实施以消除在此固有的复制 通过直接构造中间结果来直接初始化 进入正在初始化的对象;见 12.2、12.8。

由于01ints,而不是As,这里的拷贝初始化属于第二个子句。编译器在A::A(int) 构造函数中找到从intA 的用户定义转换,调用它来构造A 类型的临时变量,然后使用该临时变量执行直接初始化。反过来,这意味着编译器需要为采用 A 类型的临时构造函数执行重载解析,该构造函数选择已删除的移动构造函数,这会使程序格式错误。

【讨论】:

  • 哦。谢谢。但这似乎仍然很奇怪——需要一些被声明为未使用的东西。
  • 顺便说一句,如果我需要在数组中初始化一个大型不可复制类,你知道使用什么好的模式吗?我有一个带有 unique_ptr 向量的类。我怎样才能在数组中初始化它?这个类非常大,我真的不想为它写一个复制/移动构造函数。
  • @user3544995 unique_ptrs 的向量应该是可移动的。
  • 看看我的问题中的第二个例子——有一个带有unique_ptr的类。并且无法初始化该类的数组(因为 unique_ptr 具有已删除的复制构造函数)。是否知道如何在不复制整个类的构造函数的情况下完成它(例如它很大)?
  • @user3544995 works for me 对类定义末尾缺少的分号取模。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-06-29
  • 1970-01-01
  • 2013-08-11
  • 2015-06-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多