Любая разница между инициализацией списка копий и традиционной инициализацией копирования?

За исключением поддержки нескольких аргументов, запрещающих сужение преобразования, сопоставления конструктора, принимающего аргумент std:: initializer_list, что еще отличается для инициализации списка копий от традиционной инициализации копирования?

Чтобы быть конкретным, предположим, что существуют два пользовательских типа, A и B:

class A {...};
class B {...};

B b;
A a1 = {b};
A a2 = b;

Какое определение A и B повлияет на эти две формы инициализации? например Есть ли определенное определение A и B, которое сделает одну из инициализации легальной, а другую незаконной или юридической, но с другой семантикой, или же незаконной с разными причинами?

(Предположим, что A не имеет конструктора с аргументом std:: initializer_list.)

EDIT: добавление ссылки на несколько связанный с этим вопрос: Каково предполагаемое поведение инициализации списка копий в случае инициализатора с оператором преобразования?

Ответ 1

Вероятно, поведение новой инициализации списка копий было определено как "хорошее" и последовательное, но "странное" поведение старой инициализации копирования не могло быть изменено из-за обратной совместимости.
Как вы можете видеть, правила для инициализации списка в этом разделе идентичны для прямых и копируемых форм.
Разница, связанная с explicit, описана только в главе о разрешении перегрузки. Но для традиционной инициализации прямые и скопированные формы не идентичны. Традиционные и скользящие инициализации определяются отдельно, поэтому всегда есть вероятность некоторых (возможно, непреднамеренных) тонких различий.

Различия, которые я вижу из выдержек стандарта:

1. Уже упомянутые различия

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

    struct A
    {
        A(int i_) : i (i_) {}
        A(std::initializer_list<int> il) : i (*il.begin() + 1) {}
        int i;
    }
    
    A a1 = 5; // a1.i == 5
    A a2 = {5}; // a2.i = 6
    


2. Различное поведение для агрегатов

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

    struct Aggr
    {
        int i;
    };

    Aggr aggr;
    Aggr aggr1 = aggr; // OK
    Aggr aggr2 = {aggr}; // ill-formed


3. Различное поведение для инициализации ссылок в присутствии оператора преобразования

Инициализация скобок не может использовать операторы преобразования в ссылочный тип

struct S
{
    operator int&() { return some_global_int;}
};

int& iref1 = s; // OK
int& iref2 = {s}; // ill-formed


4. Некоторые тонкие различия в инициализации объекта типа класса объектом другого типа

Эти различия отмечены [*] в выдержках Стандарта в конце этого ответа.

  • В старой инициализации используется понятие пользовательских последовательностей преобразования (и, в частности, требуется наличие конструктора копирования, как было упомянуто)
  • Инициализация скобок просто выполняет разрешение перегрузки среди применимых конструкторов, то есть инициализация скобок не может использовать операторы преобразования в тип класса

Эти различия несут ответственность за некоторые не очень очевидные (для меня) случаи вроде

struct Intermediate {};

struct S
{
    operator Intermediate() { return {}; }
    operator int() { return 10; }
};

struct S1
{
    S1(Intermediate) {}
};

S s;
Intermediate im1 = s; // OK
Intermediate im2 = {s}; // ill-formed
S1 s11 = s; // ill-formed
S1 s12 = {s}; // OK

// note: but brace initialization can use operator of conversion to int
int i1 = s; // OK
int i2 = {s}; // OK


5. Разница в разрешении перегрузки

  • Различная трактовка явных конструкторов

См. 13.3.1.7 Инициализация с помощью инициализации списка

В инициализации списка копий, если выбран конструктор explicit, инициализация плохо сформирована. [Примечание: это отличается от других ситуаций (13.3.1.3, 13.3.1.4), где только преобразования конструкторов рассматриваются для инициализации копии. Это ограничение применяется только если эта инициализация является частью конечного результата перегрузки разрешающая способность. - конечная нота]

Если вы видите больше различий или каким-то образом корректируете мой ответ (включая грамматические ошибки), пожалуйста, сделайте.


Вот соответствующие (но длинные) выдержки из текущего проекта стандарта С++ (я не нашел способ скрыть их под спойлером): < ш > Все они находятся в главе 8.5 "Инициализаторы"

8.5 Инициализаторы

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

  • Если тип назначения является ссылочным типом, см. 8.5.3.

  • Если тип назначения - это массив символов, массив char16_t, массив char32_t или массив wchar_t, а инициализатор - это строковый литерал, см. 8.5.2.

  • Если инициализатор (), объект значение инициализации.

  • В противном случае, если тип назначения является массивом, программа плохо сформирована.

  • Если тип адресата (возможно, cv-qualified) тип класса:

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

    • [*]В противном случае (т.е. Для оставшиеся случаи инициализации копии), пользовательское преобразование последовательности, которые могут конвертировать из типа источника в место назначения type или (когда используется функция преобразования) в производный класс из них перечислены, как описано в 13.3.1.4, и лучший из них выбирается с помощью разрешения перегрузки (13.3). Если преобразование не может быть сделано или неоднозначно, инициализация плохо сформирована. Функция selected вызывается с выражением инициализатора в качестве аргумента; если функция является конструктором, вызов инициализирует временную cv-неквалифицированная версия типа назначения. Временным является prvalue. Результат вызова (который является временным для конструктор) затем используется для прямой инициализации, согласно правил выше, объект, который является местом назначения копирования инициализации. В некоторых случаях допускается реализация для устранения копирования, присущего этой прямой инициализации, посредством построение промежуточного результата непосредственно в объект, являющийся инициализируется; см. 12.2, 12.8.

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

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


8.5.3 Ссылки...


8.5.4 Инициализация списка

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

  • Если T является агрегатом, агрегатная инициализация (8.5.1).

  • В противном случае, если в списке инициализаторов нет элементы и T - это тип класса с конструктором по умолчанию, объект инициализируется значением.

  • В противном случае, если T является специализацией std::initializer_list<E> объект prvalue initializer_listпостроенный, как описано ниже, и используется для инициализации объекта в соответствии с правилами инициализации объекта из класса тот же тип (8.5).

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

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

  • В противном случае, если T является ссылочным типом, временным значением типа, на который ссылается Tинициализируется или инициализируется списком-списком, в зависимости от вид инициализации для ссылки, и ссылка привязана к этому временному. [Примечание. Как обычно, привязка не будет выполнена, и программа плохо сформирована, если ссылочный тип является ссылкой lvalue на неконстантный тип. - end note  ]

  • В противном случае, если список инициализаторов не имеет элементов, объект инициализируется значением.

  • В противном случае программа плохо сформирована.


Ответ 2

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

class B {};
struct A 
{
  A(B const&) {}
  A(A const&) = delete;
};

B b;
A a1 = {b};  // this compiles
A a2 = b;    // this doesn't because of deleted copy-ctor

Это связано с тем, что инициализация списка копий идентична инициализации прямого списка, за исключением одной ситуации - если A(B const&) был explicit, первый из них потерпел неудачу, а последний будет работать.

class B {};
struct A 
{
  explicit A(B const&) {}
};


int main()
{
    B b;
    A a1{b};    // compiles
    A a2 = {b}; // doesn't compile because ctor is explicit
}