Понимание инициализации копирования и неявных преобразований - программирование

Понимание инициализации копирования и неявных преобразований

У меня возникли проблемы с пониманием, почему следующая инициализация копии не компилируется:

#include <memory>

struct base{};
struct derived : base{};

struct test
{
    test(std::unique_ptr<base>){}
};

int main()
{
    auto pd = std::make_unique<derived>();
    //test t(std::move(pd)); // this works;
    test t = std::move(pd); // this doesn't
}

unique_ptr<derived> может быть перемещена в unique_ptr<base>, так почему второй оператор работает, а последний нет? Не явные конструкторы не учитываются при выполнении инициализации копирования?

Ошибка из gcc-8.2.0:

conversion from 'std::remove_reference<std::unique_ptr<derived, std::default_delete<derived> >&>::type' 
{aka 'std::unique_ptr<derived, std::default_delete<derived> >'} to non-scalar type 'test' requested

а из clang-7.0.0 есть

candidate constructor not viable: no known conversion from 'unique_ptr<derived, default_delete<derived>>' 
to 'unique_ptr<base, default_delete<base>>' for 1st argument

Живой код доступен здесь.

4b9b3361

Ответ 1

Тип std::unique_ptr<base> отличается от типа std::unique_ptr<derived>. Когда вы делаете

test t(std::move(pd));

Вы вызываете std::unique_ptr<base> конструктор преобразования для преобразования pd в std::unique_ptr<base>. Это нормально, так как вам разрешено одно пользовательское преобразование.

В

test t = std::move(pd);

Вы делаете инициализацию копирования, поэтому вам нужно конвертировать pd в test. Это требует 2 пользовательских преобразований, и вы не можете этого сделать. Сначала вы должны преобразовать pd в std::unique_ptr<base> а затем вам нужно преобразовать его в test. Это не очень интуитивно, но когда у вас есть

type name = something;

что бы something ни было, это должно быть только одно преобразование, определенное пользователем, из типа источника. В вашем случае это означает, что вам нужно

test t = test{std::move(pd)};

который использует только одного неявного пользователя, определенного как первый случай.


Давайте удалим std::unique_ptr и рассмотрим в общем случае. Поскольку std::unique_ptr<base> не совпадает с типом std::unique_ptr<derived> мы по существу имеем

struct bar {};
struct foo
{ 
    foo(bar) {} 
};

struct test
{
    test(foo){}
};

int main()
{
    test t = bar{};
}

и мы получаем ту же ошибку, потому что нам нужно перейти от bar → foo → test и у него слишком много одного пользовательского преобразования.

Ответ 2

Семантика инициализаторов описана в [dcl.init] №17. Выбор прямой инициализации или копии инициализации приводит нас к одной из двух разных целей:

Если тип назначения является (возможно, cv-квалифицированным) типом класса:

  • [...]

  • В противном случае, если инициализация является прямой инициализацией, или если это инициализация копирования, где cv-неквалифицированная версия исходного типа является тем же классом или производным классом класса назначения, конструкторы рассматриваются. Применимые конструкторы перечисляются ([over.match.ctor]), и лучший выбирается через разрешение перегрузки. Выбранный таким образом конструктор вызывается для инициализации объекта, с выражением инициализатора или списком выражений в качестве аргументов. Если конструктор не применяется, или разрешение перегрузки неоднозначно, инициализация некорректна.

  • В противном случае (т.е. Для остальных случаев инициализации копирования) определяемые пользователем последовательности преобразования, которые могут преобразовывать тип источника в тип назначения или (если используется функция преобразования) в его производный класс, перечисляются, как описано в [over.match.copy], а лучший выбирается через разрешение перегрузки. Если преобразование не может быть выполнено или является неоднозначным, инициализация неверна. Выбранная функция вызывается с выражением инициализатора в качестве аргумента; если функция является конструктором, то вызов является значением cv-неквалифицированной версии целевого типа, чей результирующий объект инициализируется конструктором. Вызов используется для прямой инициализации, согласно приведенным выше правилам, объекта, являющегося местом назначения инициализации копирования.

В случае прямой инициализации мы вводим первый цитируемый маркер. Как там подробно описано, конструкторы рассматриваются и перечисляются напрямую. Следовательно, неявная последовательность преобразования требуется только для преобразования unique_ptr<derived> в unique_ptr<base> в качестве аргумента конструктора.

В случае инициализации копирования мы больше не рассматриваем непосредственно конструкторы, а скорее пытаемся выяснить, какая неявная последовательность преобразования возможна. Доступно только одно: unique_ptr<derived> для unique_ptr<base> для test. Поскольку неявная последовательность преобразования может содержать только одно пользовательское преобразование, это недопустимо. Как таковая, инициализация плохо сформирована.

Можно сказать, что с помощью прямой инициализации своего рода "обходит" одно неявное преобразование.

Ответ 3

Я уверен, что компилятор может рассматривать только одно неявное преобразование. В первом случае требуется только преобразование из std::unique_ptr<derived>&& в std::unique_ptr<base>&&, во втором случае базовый указатель также необходимо преобразовать в test (для работы конструктора перемещения по умолчанию). Так, например, преобразование производного указателя в base: std::unique_ptr<base> bd = std::move(pd) а затем перемещение, назначая его, также будет работать.