Конструктор С++ Copy вызывается вместо initializer_list <>

На основе этого кода

struct Foo 
{
   Foo() 
   {
       cout << "default ctor" << endl;
   }

   Foo(std::initializer_list<Foo> ilist) 
   {
       cout << "initializer list" << endl;
   }

   Foo(const Foo& copy)
   {
       cout << "copy ctor" << endl;
   }
};

int main()
{

   Foo a;
   Foo b(a); 

   // This calls the copy constructor again! 
   //Shouldn't this call the initializer_list constructor?
   Foo c{b}; 



   _getch();
   return 0;
}

Вывод:

default ctor

копировать ctor

копировать ctor

В третьем случае я помещаю b в инициализацию скобки, которая должна вызывать конструктор initializer_list < > .

Вместо этого используется конструктор копирования.

Кто-нибудь из вас расскажет мне, как это работает и почему?

Ответы

Ответ 1

Как отметил Никол Болас, исходная версия этого ответа была неправильной: cppreference на момент написания неправильно документировал порядок, в котором конструкторы рассматривались в инициализации списка. Ниже приведен ответ с использованием правил, поскольку они существуют в черновом проекте стандарта n4140, который очень близок к официальному стандарту С++ 14.

Текст оригинального ответа по-прежнему включен для записи.


Обновленный ответ

В комментарии Пер Натана Оливера gcc и clang производят разные результаты в этой ситуации:

g++ -std=c++14 -Wall -pedantic -pthread main.cpp && ./a.out
default ctor
copy ctor
copy ctor
initializer list


clang++ -std=c++14 -Wall -pedantic -pthread main.cpp && ./a.out
default ctor
copy ctor
copy ctor

gcc верен.

n4140 [dcl.init.list]/1

Инициализация списка - это инициализация объекта или ссылки из списка с привязкой к init.

Вы используете там инициализацию списка, а так как c - объект, правила его инициализации списка определены в [dcl.init.list]/3:

[Dcl.init.list]/3:

Инициализация списка объекта или ссылки типа T определяется следующим образом:

  1. Если T является совокупностью...
  2. В противном случае, если в списке инициализаторов нет элементов...
  3. В противном случае, если T является специализацией std::initializer_list<E>...

просматривая список до сих пор:

  1. Foo не является совокупностью.
  2. Он имеет один элемент.
  3. Foo не является специализацией std::initializer_list<E>.

Затем мы нажмем [dcl.init.list]/3.4:

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

Теперь мы куда-то попадаем. 13.3.1.7 также известен как [over.match.list]:

Инициализация с помощью инициализации списка
Когда объекты неагрегатного типа типа T инициализируются списком (8.5.4), разрешение перегрузки выбирает конструктор в две фазы:

  1. Первоначально функции-кандидаты являются конструкторами-инициализаторами-списками (8.5.4) класса T а список аргументов состоит из списка инициализаторов как одного аргумента.
  2. Если не найден жизнеспособный конструктор списка инициализаторов, разрешение перегрузки выполняется снова, где функции-кандидаты являются всеми конструкторами класса T а список аргументов состоит из элементов списка инициализаторов.

Таким образом, конструктор копирования будет учитываться только после конструкторов списка инициализаторов во второй фазе разрешения перегрузки. Здесь должен использоваться конструктор списка инициализаторов.

Стоит отметить, что [over.match.list] затем продолжается:

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

и что после [dcl.init.list]/3. 5 относится к одноэлементной инициализации списка:

В противном случае, если в списке инициализаторов есть один элемент типа E и либо T не является ссылочным типом, либо его ссылочный тип связан с ссылкой на E, объект или ссылка инициализируются из этого элемента; если для преобразования элемента в T требуется преобразование сужения (см. ниже), программа плохо сформирована.

в котором объясняется, где cppreference получил свой специальный случай для инициализации одноэлементного списка, хотя они поместили его выше в порядке, чем это должно быть.


Оригинальный ответ

Вы сталкиваетесь с интересным аспектом инициализации списка, где, если список выполняет определенные требования, он может рассматриваться как инициализация копирования, а не инициализация списка.

из cppreference:

Эффекты инициализации списка объекта типа T:

Если T - тип класса, а в списке инициализаторов есть один элемент того же или производного типа (возможно, cv-квалифицированный), объект инициализируется из этого элемента (путем инициализации копирования для инициализации списка копий или путем прямого преобразования, инициализация для инициализации прямого списка). (поскольку С++ 14)

Foo c{b} выполняет все эти требования.

Ответ 2

Давайте рассмотрим, что спецификация С++ 14 говорит о инициализации списка здесь. [dcl.init.list] 3 имеет последовательность правил, которые должны применяться по порядку:

3.1 не применяется, поскольку Foo не является агрегатом.

3.2 не применяется, поскольку список не пуст.

3.3 не применяется, так как Foo не является специализацией initializer_list.

3.4 применяется, поскольку Foo - тип класса. В нем говорится о конструкторах с разрешением перегрузки в соответствии с [over.match.list]. И это правило говорит, чтобы сначала проверить конструкторы initializer_list. Поскольку ваш тип имеет конструктор initilaizer_list, компилятор должен проверить, может ли быть изготовлен initializer_list соответствующий один из этих конструкторов из заданных значений. Он может, поэтому это то, что нужно называть.

Короче говоря, GCC прав, а Clang ошибочен.

Следует отметить, что рабочий проект С++ 17 ничего не меняет. Он имеет новый раздел 3.1, который имеет специальную формулировку для списков с одним значением, но применим только к агрегатам. Foo не является агрегатом, поэтому он не применяется.