Что такое правило копирования или перемещения конструктора на С++? Когда происходит переход от копирования к копии?

Первый пример:

#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.

Удалите конструктор перемещения и добавьте копию:

#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;
}

Теперь компиляция падает с ошибкой "использование удаленной функции" A:: A (A & &) "
Поэтому конструктор MOVE НЕОБХОДИМО и не возвращается к конструктору COPY.

Теперь давайте удалим как copy-и move-constructors:

#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;
}

И он падает с "ошибкой компиляции удаленной функции A:: A (const A &)". Теперь он ТРЕБУЕТ конструктора COPY!
Таким образом, резервный (?) От конструктора перемещения был создан конструктором копирования.

Зачем? Кто-нибудь знает, как он соответствует стандарту С++ и каково фактически правило выбора между конструкторами копирования/перемещения?

Ответ 1

Нет "резервной". Это называется разрешением перегрузки. Если в разрешении перегрузки есть более одного кандидата, то выбирается наилучшее совпадение в соответствии со сложным набором правил, который вы можете найти, прочитав стандарт С++ или черновик.

Вот пример без конструкторов.

class X { };

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

int main()
{
    func( X{} );
}
  • As-is: печатает "move"
  • Комментарий "1": печатает "копировать"
  • Комментарий "2": печатает "move"
  • Комментарий "1" и "2": не удается скомпилировать

При разрешении перегрузки привязка rvalue к rvalue имеет более высокое предпочтение, чем lvalue to rvalue.


Вот очень похожий пример:

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

int main()
{
     func(1);
}
  • As-is: печатает "int"
  • Комментарий "1": печатает "длинный"
  • Комментарий "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 & &), потому что у вас есть деструктор.

Для разрешения перегрузки не имеет значения, существует ли тело функции или нет, то является ли функция объявлена ​​(явно или неявно).

Ответ 2

Функция, которую он будет использовать, выбирается, прежде чем проверять, есть ли она delete d или нет. В случае, если конструктор копирования был доступен, а конструктор перемещения был delete d, конструктор перемещения по-прежнему был лучшим выбором из двух. Затем он видит, что он delete d и дает вам ошибку.

Если у вас был тот же пример, но на самом деле удалил конструктор перемещения, вместо того, чтобы сделать его delete d, вы увидите, что он компилируется отлично и возвращается к использованию конструктора копирования:

#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;
}

У этого класса нет конструктора перемещения, объявленного для него вообще (даже неявно), поэтому его нельзя выбрать.