Учитывая следующее:
#include <stdio.h>
class X;
class Y
{
public:
Y() { printf(" 1\n"); } // 1
// operator X(); // 2
};
class X
{
public:
X(int) {}
X(const Y& rhs) { printf(" 3\n"); } // 3
X(Y&& rhs) { printf(" 4\n"); } // 4
};
// Y::operator X() { printf(" operator X() - 2\n"); return X{2}; }
int main()
{
Y y{}; // Calls (1)
printf("j\n");
X j{y}; // Calls (3)
printf("k\n");
X k = {y}; // Calls (3)
printf("m\n");
X m = y; // Calls (3)
printf("n\n");
X n(y); // Calls (3)
return 0;
}
Все идет нормально. Теперь, если я включу оператор преобразования Y::operator X()
Я понял это; —
X m = y; // Calls (2)
Насколько я понимаю, это происходит потому, что (2) является «менее постоянным», чем (3) и
поэтому предпочтительнее. Призыв к X
конструктор исключен
Мой вопрос, почему не определение X k = {y}
изменить свое поведение таким же образом? я знаю это = {}
технически «инициализация копии списка», но в отсутствие конструктора, принимающего initializer_list
типа, разве это не возвращает к «копированию инициализации»? то есть — так же, как и для X m = y
Где дыра в моем понимании?
Где дыра в моем понимании?
tltldr; Никто не понимает инициализацию.
tldr; Предпочитает инициализацию списка std::initializer_list<T>
конструкторы, но это не отступает от не-списочной инициализации. Это только возвращается к рассмотрению конструкторов. При инициализации без списков учитываются функции преобразования, а в качестве альтернативы — нет.
Все правила инициализации происходят из [Dcl.init]. Итак, давайте просто перейдем от первых принципов.
- Если инициализатор (без скобок) приготовился-INIT-лист или есть = приготовился-INIT-лист, объект или ссылка инициализируются списком.
Первый первый пункт относится к любой инициализации списка. Это прыгает X x{y}
а также X x = {y}
к [Dcl.init.list]. Мы вернемся к этому. Другой случай проще. Давайте посмотрим на X x = y
, Мы звоним прямо в:
- В противном случае (то есть для остальных случаев инициализации копирования) определяемые пользователем последовательности преобразования, которые могут преобразовывать из типа источника в тип назначения или (когда используется функция преобразования) в его производный класс, перечисляются, как описано в [Over.match.copy], и лучший выбирается через разрешение перегрузки.
Кандидаты в [over.match.copy] являются:
- Конвертирующие конструкторы
T
[в нашем случае,X
] являются кандидатами функций.- Когда тип выражения инициализатора является типом класса «резюме
S
”, Неявные функции преобразованияS
и его базовые классы рассматриваются.В обоих случаях список аргументов имеет один аргумент, который является выражением инициализатора.
Это дает нам кандидатов:
X(Y const &); // from the 1st bullet
Y::operator X(); // from the 2nd bullet
2-й эквивалентно тому, чтобы иметь X(Y& )
, поскольку функция преобразования не является cv-квалифицированной. Это делает ссылку менее квалифицированной по cv, чем конструктор преобразования, так что это предпочтительнее. Обратите внимание, что нет вызова X(X&& )
здесь, в C ++ 17.
Теперь вернемся к случаям инициализации списка. Первая соответствующая точка [Dcl.init.list] /3.6:
В противном случае, если
T
является типом класса, конструкторы рассматриваются. Применимые конструкторы перечисляются, и лучший выбирается через разрешение перегрузки ([over.match], [over.match.list]). Если для преобразования какого-либо из аргументов требуется сужающее преобразование (см. Ниже), программа является некорректной.
что в обоих случаях приводит нас к [Over.match.list] который определяет двухфазное разрешение перегрузки:
- Первоначально функции-кандидаты являются конструкторами списка инициализаторов ([dcl.init.list]) класса T, а список аргументов состоит из списка инициализаторов как единственного аргумента.
- Если жизнеспособный конструктор списка инициализаторов не найден, снова выполняется разрешение перегрузки, где все функции-кандидаты являются конструкторами класса T, а список аргументов состоит из элементов списка инициализатора.
Если список инициализаторов не имеет элементов, а T имеет конструктор по умолчанию, первая фаза опускается. При инициализации copy-list-init, если выбран явный конструктор, инициализация некорректна.
Кандидаты являются конструкторами X
, Единственная разница между X x{y}
а также X x = {y}
в том, что если последний выбирает explicit
конструктор, инициализация некорректна. У нас даже нет explicit
конструкторы, так что оба эквивалентны. Следовательно, мы перечисляем наши конструкторы:
X(Y const& )
X(X&& )
в порядке Y::operator X()
Первый — это прямая ссылка, которая является точным соответствием. Последнее требует пользовательского преобразования. Следовательно, мы предпочитаем X(Y const& )
в этом случае.
Обратите внимание, что gcc 7.1 делает это неправильно в режиме C ++ 1z, поэтому я подал ошибка 80943.
Мой вопрос: почему определение X k = {y} не меняет свое поведение таким же образом?
Потому что, концептуально говоря, = { .. }
является инициализация за то, что автоматически выбирает «лучший» способ инициализации цели из брекетов, в то время как = value
это также инициализация, но концептуально также преобразование значение к другому значению. Преобразование является полностью симметричным: If изучит исходное значение, чтобы увидеть, предоставляет ли он способ создания цели, и изучит цель, чтобы увидеть, предоставляет ли он способ принятия источника.
Если ваш целевой тип struct A { int x; }
затем с помощью = { 10 }
не будет пытаться преобразовать 10
в A
(который потерпит неудачу). Но он будет искать лучшую (с их точки зрения) форму инициализации, которая здесь составляет совокупную инициализацию. Однако если A
не является агрегатом (добавить конструкторы), тогда он будет вызывать конструкторы, где в вашем случае он находит Y
принимаются с готовностью без необходимости конвертации. Там нет такой симметрии между источником и целью, как это происходит с преобразованием при использовании = value
форма.
Ваше подозрение о «менее постоянном» функции преобразования совершенно верно. Если вы сделаете функцию преобразования постоянным членом, то она станет неоднозначной.