C++中的复制/移动构造函数选择规则是什么?何时会发生移动到复制的回退?

15

第一个例子:

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

struct A {
    unique_ptr<int> ref;
    A(const A&) = delete;
    A(A&&) = default;
    A(const int i) : ref(new int(i)) { }
    ~A() = default;
};

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

它完美地运作。因此,在这里使用了MOVE构造函数。

让我们移除MOVE构造函数并添加一个复制构造函数:

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

struct A {
    unique_ptr<int> ref;
    A(const A&a) 
        : ref( a.ref.get() ? new int(*a.ref) : nullptr )
    {  }
    A(A&&) = delete;
    A(const int i) : ref(new int(i)) { }
    ~A() = default;
};

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

现在编译出现错误 "use of deleted function ‘A::A(A&&)’"
因此需要移动构造函数,不能退回到复制构造函数。

现在我们将删除复制和移动构造函数:

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

struct A {
    unique_ptr<int> ref;
    A(const int i) : ref(new int(i)) { }
    ~A() = default;
};

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

编译器报错时会提示“use of deleted function ‘A::A(const A&)’”,这意味着现在需要一个COPY构造函数!
所以现在必须从移动构造函数退回到复制构造函数。

为什么?有没有人知道它如何符合C++标准,以及在选择复制/移动构造函数时实际上的规则是什么?


1
我半个小时前不是已经回答了完全相同的问题吗 >.> - M.M
这个问题更加具体,不是同一个问题。 - Sap
2个回答

11

在检查函数是否被删除之前,选择将要使用的函数。如果复制构造函数可用而移动构造函数被标记为delete,那么在这两个函数中,移动构造函数仍然是更好的选择。然后,程序发现它已经被delete了,并给出一个错误。

如果您有相同的示例,但实际上删除了移动构造函数,而不是将其标记为delete,您会看到它可以编译,然后回退使用复制构造函数:

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

struct A {
    unique_ptr<int> ref;
    A(const A&a) 
        : ref( a.ref.get() ? new int(*a.ref) : nullptr )
    {  }
    A(const int i) : ref(new int(i)) { }
    ~A() = default;
};

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

这个类根本没有移动构造函数(甚至没有隐式声明的),因此它无法被选择。


那么选择(选择过程)取决于它有一个构造函数还是两个?这似乎很奇怪。因此,删除的构造函数和不存在的构造函数不是同一回事吗? - Sap
2
@user3544995,选择过程取决于声明了哪些构造函数。它将选择最佳匹配的构造函数,就像在所有重载情况下一样。是的,deleted构造函数和不存在的构造函数不一定相同。虽然可能会将缺少的构造函数隐式声明为deleted,但这并非必然。在这种情况下,它只是根本没有被隐式声明。 - Joseph Mansfield

11

没有所谓的“备选方案”。它被称为重载决策。如果在重载决策中有一个以上的可能候选项,那么将选择最佳匹配项,根据一系列复杂的规则进行选择,这些规则可在阅读C++标准或其草案时找到。

以下是一个没有构造函数的示例。

class X { };

void func(X &&) { cout << "move\n"; }            // 1
void func(X const &)  { cout << "copy\n"; }      // 2

int main()
{
    func( X{} );
}
  • 原文: 输出 "move"
  • 注释掉 "1": 输出 "copy"
  • 注释掉 "2": 输出 "move"
  • 注释掉 "1" 和 "2": 编译失败

在重载决议中,将右值绑定到右值的优先级高于将左值绑定到右值。


这里是一个非常相似的例子:

void func(int) { cout << "int\n"; }      // 1
void func(long) { cout << "long\n"; }    // 2

int main()
{
     func(1);
}
  • 现状:输出 "int"
  • 注释掉 "1":输出 "long"
  • 注释掉 "2":输出 "int"
  • 注释掉 "1" 和 "2":编译失败

在重载决议中,精准匹配的函数优先于转换。


在这个线程的三个示例中,我们有:

1:两个备选函数;rvalue 更倾向 rvalue(就像我的第一个示例一样)

A(const A&);
A(A&&);           // chosen

2: 两个候选函数; rvalue更喜欢rvalue(就像我的第一个示例中那样)

A(const A&); 
A(A&&);           // chosen

3:一种候选函数,无需竞争

A(const A&);      // implicitly declared, chosen

如之前所解释的,第3种情况中没有A(A&&)的隐式声明,因为你有一个析构函数。

对于重载分辨率,函数体是否存在并不重要,重要的是函数是否被声明(无论是显式还是隐式)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接