Что такое правило копирования или перемещения конструктора на С++? Когда происходит переход от копирования к копии?
Первый пример:
#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;
}
У этого класса нет конструктора перемещения, объявленного для него вообще (даже неявно), поэтому его нельзя выбрать.