Скопируйте инициализацию формы '= {}'

Учитывая следующее:

#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

Где дыра в моем понимании?

2 ответа

Решение

Где дыра в моем понимании?

tltldr; Никто не понимает инициализацию.

tldr; Предпочитает инициализацию списка std::initializer_list<T> конструкторы, но это не отступает от не-списочной инициализации. Это только возвращается к рассмотрению конструкторов. При инициализации без списков учитываются функции преобразования, а в качестве альтернативы - нет.


Все правила инициализации взяты из [dcl.init]. Итак, давайте просто перейдем от первых принципов.

[dcl.init] /17.1:

  • Если инициализатором является (не заключенный в скобки) фигурный список инициализации или is = braced-init-list, объект или ссылка инициализируются списком.

Первый первый пункт относится к любой инициализации списка. Это прыгает X x{y} а также X x = {y} к [dcl.init.list]. Мы вернемся к этому. Другой случай проще. Давайте посмотрим на X x = y, Мы звоним прямо в:

[dcl.init] /17.6.3:

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

Кандидаты в [over.match.copy] являются:

  • Конвертирующие конструкторы T [в нашем случае, X] являются кандидатами функций.
  • Когда тип выражения инициализатора является типом класса “cv 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 форма.

Ваше подозрение о "менее постоянном" функции преобразования совершенно верно. Если вы сделаете функцию преобразования постоянным членом, то она станет неоднозначной.

Другие вопросы по тегам