Почему инициализация члена на месте использует конструктор копирования в С++ 11?

Я немного запутался в следующем коде:

struct A {
  std::atomic<int> a = 0;
};

Что дает сообщение об ошибке:

копирование субобъекта элемента типа 'std:: atomic' вызывает удаленный конструктор

Но почти тот же код работает:

struct A {
  std::atomic<int> a = {0};
};

Okey, если для первого варианта требуется конструктор копирования, тогда он должен использовать operator=(). Но ждать! Этот оператор отлично работает без конструктора копирования:

A a;
a.a = 1;

Может ли кто-нибудь объяснить, как обе инициализации на месте расширяются с точки зрения простых операций? Почему первый требует конструктора копирования?

Ответы

Ответ 1

Все ссылки на N3797, текущий рабочий проект С++ 1y. §8.5 Инициализаторы [dcl.init]/15:

Инициализация, которая встречается в форме

T x = a;

а также при передаче аргументов, возврату функции, выдаче исключения (15.1), обработке исключения (15.3), а инициализация агрегатного члена (8.5.1) называется копированием-инициализацией. [Примечание. Копирование-инициализация может вызвать ход (12.8). -end note]

Итак, декларация:

std::atomic<int> a = 0;

выполняет инициализацию копирования. Согласно 8.5/17:

Семантика инициализаторов такова. Тип назначения - тип инициализированного объекта или ссылки, а тип источника - тип выражения инициализатора. Если инициализатор не является одним (возможно, в скобках) выражением, тип источника не определен.

Тип назначения здесь std::atomic<int>, а тип источника - int (т.е. decltype(0)). Чтобы определить семантику инициализации, мы должны определить, какая из паттернов пункта 17 применяется:

  • Если инициализатор представляет собой (не заключенный в скобки) бит-init-list, объект или ссылка инициализируется списком (8.5.4).
  • Если тип назначения является ссылочным типом, см. 8.5.3.
  • Если тип назначения - это массив символов, массив из char16_t, массив char32_t или массив wchar_t, а инициализатор - строковый литерал, см. 8.5.2.
  • Если инициализатор (), объект инициализируется значением.
  • В противном случае, если тип назначения является массивом, программа плохо сформирована.
  • Если тип назначения является (возможно, cv-квалифицированным) типом класса:
    • Если инициализация является прямой инициализацией или если она является копией-инициализацией, где cv-неквалифицированная версия типа источника является тем же классом, что или производным классом класса назначения,... [ не применяется, тип источника int]
    • В противном случае (т.е. для остальных случаев инициализации копии) пользовательские последовательности преобразования, которые могут преобразовываться из типа источника в тип назначения или (когда функция преобразования ) к его производному классу перечислены, как описано в 13.3.1.4, и лучший выбирается с помощью разрешения перегрузки (13.3). Если преобразование невозможно или неоднозначно, инициализация плохо сформирована. Выбранная функция вызывается с выражением инициализатора в качестве аргумента; если функция является конструктором, вызов инициализирует временную версию cv-unqualified версии целевого типа. Временной является prvalue. Результат вызова (который является временный для случая конструктора) затем используется для прямой инициализации, в соответствии с вышеприведенными правилами, объект, который является местом назначения инициализации копирования. В некоторых случаях реализация разрешено исключать копирование, присущее этой прямой инициализации, путем создания промежуточный результат непосредственно в инициализированный объект; см. 12.2, 12.8.
  • ...

Вот мы. Инициализационное выражение - 0 - преобразуется в std::atomic<int> посредством создания временного объекта, инициализированного конструктором std::atomic<int>(int). Этот временный объект используется для прямого инициализации исходного объекта std::atomic<int>. Другой из классов класса (возможно, cv-qualit) типа, которые мы игнорировали до сих пор, применяется:

  • Если инициализация представляет собой прямую инициализацию или если она является копией-инициализацией, где рассматриваются cv-неквалифицированная версия типа источника того же класса, что и производный класс класса назначения, рассматриваются конструкторы. Соответствующие конструкторы перечислены (13.3.1.3), а лучший выбирается с помощью разрешения перегрузки (13.3). Выбранный таким образом конструктор вызывается для инициализации объекта с выражением инициализатора или списком выражений в качестве аргумента (ов). Если конструктор не применяется или разрешение перегрузки неоднозначно, инициализация плохо сформирована.

Напомним, что новый инициализатор является prvalue std::atomic<int>. Разрешение перегрузки определяет, что не существует соответствующего конструктора std::atomic<int>, который принимает один аргумент std::atomic<int>&& (std::atomic<int> не может перемещаться или копироваться) и диагностирует программу как не сформированную.

Для второй части вопроса

std::atomic<int> a = {0};

- снова инициализация копирования на 8.5/15. На этот раз, однако, применяется самая первая пуля 8.5/17:

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

Для инициализации списка, мы должны посмотреть на 8.5.4/3:

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

  • Если T является агрегатом, выполняется агрегатная инициализация (8.5.1).
  • В противном случае, если в списке инициализаторов нет элементов, а T - это тип класса с конструктором по умолчанию, объект инициализируется значением.
  • В противном случае, если T является специализацией std::initializer_list<E>, объект prvalue initializer_list строится, как описано ниже, и используется для инициализации объекта в соответствии с правилами инициализации объекта из класса того же типа (8.5).
  • В противном случае, если T - тип класса, рассматриваются конструкторы. Применяемые конструкторы перечисляются, а лучший выбирается с помощью разрешения перегрузки (13.3, 13.3.1.7). Если для преобразования любого из аргументов требуется сужение преобразования (см. Ниже), программа плохо сформирована.
  • ...

std::atomic<int> - это тип класса, а не агрегированная или initializer_list специализация, поэтому рассматриваются конструкторы. Конструктор std::atomic<int>::atomic(int) будет выбран как идеальное совпадение и используется для инициализации объекта.

Ответ 2

Рассмотрим первый случай

struct A {
  std::atomic<int> a = 0;
};

Для успешной инициализации должен быть доступный конструктор копирования. Но конструктор копирования определяется как удаленный.

atomic(const atomic&) = delete;

Итак, компилятор выдает сообщение об ошибке.

Во втором случае

struct A {
  std::atomic<int> a = {0};
};

где используется список инициализаторов, конструктор копирования не требуется. Компилятор ищет конструктор, который принимает один аргумент int, и такой конструктор действительно существует, поэтому он вызывается.

constexpr atomic(T) noexcept;

или если подставить шаблонный параметр для типа int

constexpr atomic(int) noexcept;

Согласно стандарту С++, если класс не имеет конструктора с первым параметром типа std:: initializer_list (когда указан список инициализаторов), то

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

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

В последнем случае

A a;
a.a = 1;

используется оператор присваивания

T operator=(T) noexcept;

или если подставить шаблонный параметр для типа int

int operator=(int) noexcept;

Таким образом, проблем нет.