Понимание инициализации копирования и неявных преобразований
У меня возникли проблемы с пониманием, почему следующая инициализация копии не компилируется:
#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
Живой код доступен здесь.
Ответы
Ответ 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)
а затем перемещение, назначая его, также будет работать.