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

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

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

It works perfectly. So here the MOVE constructor is used.

Let's remove the move constructor and add a copy one:

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

Now the compilation falls with the error "use of deleted function ‘A::A(A&&)’"
So the MOVE constructor is REQUIRED and there is no fall back to a COPY constructor.

Now let's remove both copy- and 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;
}

And it falls with "use of deleted function ‘A::A(const A&)’" compilation error. Now it REQUIRES a COPY constructor!
So there was a fallback (?) from the move constructor to the copy constructor.

Why? Does anyone have any idea how does it conform to the C++ standard and what actually is the rule of choosing among copy/move constructors?


person Sap    schedule 11.07.2014    source источник
comment
Разве я не ответил ровно на этот вопрос полчаса назад ›.›   -  person M.M    schedule 11.07.2014
comment
Этот вопрос гораздо конкретнее. Это не тот же вопрос.   -  person Sap    schedule 11.07.2014


Ответы (2)


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

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

class X { };

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

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

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


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

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&&), поскольку у вас есть деструктор.

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

person M.M    schedule 11.07.2014

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

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

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

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

person Joseph Mansfield    schedule 11.07.2014
comment
Итак, выбор (процесс выбора) зависит от того, есть ли у него один конструктор или два? Это кажется проводным. Значит, удаленный конструктор и отсутствующий конструктор — это не одно и то же? - person Sap; 11.07.2014
comment
@ user3544995 Процесс выбора зависит от того, какие конструкторы объявлены. Он выберет наиболее подходящий конструктор, как и во всех ситуациях перегрузки. И да, конструктор deleted и отсутствующий конструктор не обязательно одно и то же. Однако отсутствующий конструктор может быть неявно объявлен как deleted. В данном случае это не так — он просто вообще не объявлен неявно. - person Joseph Mansfield; 11.07.2014