Конструктор копирования C++ вызывается вместо списка инициализаторов

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

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;
}

выход:

по умолчанию ctor

копировать ctor

копировать ctor

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

вместо этого конструктор копирования берет на себя инициативу.

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

2 ответов


как указал Николь Болас, исходная версия этого ответа была неправильной: cppreference на момент написания неправильно документировал порядок, в котором конструкторы рассматривались в инициализации списка. Ниже приведен ответ с использованием правил, как они существуют в проекте стандарта n4140, который очень близок к официальному стандарту C++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.в этом.list] / 1

List-initialization-инициализация объекта или ссылки из связанного списка инициализации.

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

[dcl.в этом.list] / 3:

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

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

проходим так далеко:

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

затем мы нажимаем [dcl.в этом.list] / 3.4:

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

теперь мы к чему-то пришли. 13.3.1.7 также известен как [окончен.спичка.список]:

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

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

поэтому конструктор копирования будет рассматриваться только после конструкторы списка инициализаторов на втором этапе разрешения перегрузки. Здесь следует использовать конструктор списка инициализаторов.

стоит отметить, что на.спичка.list] затем продолжается:

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

и что после [dcl.в этом.list] / 3.5 имеет дело с инициализацией одноэлементного списка:

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

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


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

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

С cppreference:

эффекты инициализации списка объекта типа T являются:

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

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


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

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

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

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

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

короче говоря, GCC прав и Clang не так.

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