Копировать инициализацию формы '= {}'

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

#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) меньше const, чем (3) и поэтому предпочтительнее. Вызов конструктора X отменяется

Мой вопрос: почему определение X k = {y} не меняет свое поведение таким же образом? Я знаю, что = {} является технически "инициализацией списка экземпляров", но в отсутствие конструктора, использующего тип initializer_list, не возвращается ли это к "копированию инициализации"? т.е. - то же, что и для X m = y

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

Ответы

Ответ 1

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

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

TL;DR; Инициализация списков предпочитает конструкторы std::initializer_list<T>, но не возвращается к инициализации без списка. Он возвращается к рассмотрению конструкторов. Инициализация без списка будет рассматривать функции преобразования, но резервный нет.


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

[dcl.init]/17.1:

  • Если инициализатор представляет собой (не заключенный в скобки) бит-init-list или 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

Второй эквивалент имеет X(Y& ), так как функция преобразования не соответствует критериям cv. Это приводит к меньшей ссылочной ссылке, чем конструктор преобразования, поэтому он предпочтет. Обратите внимание: здесь нет ссылки X(X&& ) на С++ 17.


Теперь вернемся к случаям инициализации списка. Первая соответствующая маркерная точка [dcl.init.list]/3.6:

В противном случае, если T - тип класса, рассматриваются конструкторы. Соответствующие конструкторы перечислены, и лучший выбирается с помощью разрешения перегрузки ([over.match], [over.match.list]). Если для преобразования любого из аргументов требуется сужение преобразования (см. Ниже), программа плохо сформирована.

который в обоих случаях приводит нас к [over.match.list], который определяет разрешение двухфазной перегрузки:

  • Изначально функции-кандидаты - это конструкторы-инициализаторы-списки ([dcl.init.list]) класса T, а список аргументов состоит из списка инициализаторов как один аргумент.
  • Если не найден жизнеспособный конструктор списка инициализаторов, разрешение перегрузки выполняется снова, где функции-кандидаты - все конструкторы класса T, а список аргументов состоит из элементов списка инициализаторов.

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

Кандидаты являются конструкторами X. Единственное различие между X x{y} и X x = {y} заключается в том, что если последний выбирает конструктор explicit, инициализация плохо сформирована. У нас даже нет конструкторов explicit, поэтому они эквивалентны. Следовательно, мы перечисляем наши конструкторы:

  • X(Y const& )
  • X(X&& ) через Y::operator X()

Первый - это прямая ссылка, которая является Точным соответствием. Последнее требует пользовательского преобразования. Следовательно, в этом случае мы предпочитаем X(Y const& ).


Обратите внимание, что gcc 7.1 ошибается в режиме С++ 1z, поэтому я зарегистрировал ошибка 80943.

Ответ 2

Вопрос в том, почему определение X k = {y} так же не меняет свое поведение?

Поскольку концептуально говоря, = { .. } является инициализацией для того, что автоматически выбирает "лучший" способ инициализировать цель из фигурных скобок, а = value также является инициализацией, но концептуально также является преобразованием значения на другое значение. Преобразование полностью симметрично: если вы посмотрите на исходное значение, чтобы узнать, обеспечивает ли он способ создания цели, и заглянет в цель, чтобы узнать, обеспечивает ли он способ принять источник.

Если ваш целевой тип struct A { int x; }, то использование = { 10 } не будет пытаться преобразовать 10 в A (что не удастся). Но он будет искать наилучшую (в их глазах) форму инициализации, которая здесь сводится к совокупной инициализации. Однако, если A не является агрегатом (добавьте конструкторы), он вызовет конструкторы, где в вашем случае он найдет Y, принятый без необходимости преобразования. Нет такой симметрии между источником и объектом, как это происходит с преобразованием при использовании формы = value.

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