Вопрос: std :: варианты initializer_list


Каковы различия между следующими тремя инициализациями с std::initializer_lists?

std::vector<int> a{ 2, 3, 5, 7};
std::vector<int> b( { 2, 3, 5, 7} );
std::vector<int> c = { 2, 3, 5, 7};

В приведенном выше примере, std::vector просто заполнитель, но меня интересует общий ответ.


5


источник


Ответы:


Отвлечемся от абстрактных std::vector, И назовите это T,

T t{a, b, c};
T t = { a, b, c };
T t({a, b, c});

Первые две формы - это инициализация списка (и только  разница между ними заключается в том, что если T является классом, для второго explicit конструкторы запрещены к вызову. Если вы вызываете, программа становится плохо сформированной). Последняя форма - просто обычная прямая инициализация, как мы ее знаем из C ++ 03:

T t(arg);

То, что появляется {a, b, c} в виде Arg  означает, что аргумент для вызова конструктора является списком инициализатора скобок. Эта третья форма не имеет специальной обработки, которую имеет инициализация списка. T  должен  быть типом класса там, даже если бит-список инициализации имеет только 1 аргумент. Я рад, что мы устанавливаем четкие правила  прежде чем выпустить C ++ 11 в этом случае.


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

struct T {
  T(int);
  T(std::initializer_list<int>);
};

T t({1});

Поскольку прямая инициализация - это просто вызов перегруженных конструкторов, мы можем преобразовать это в

void ctor(int); 
void ctor(std::initializer_list<int>);
void ctor(T const&);
void ctor(T &&);

Мы можем использовать обе трейлинг-функции, но нам понадобится преобразование, определенное пользователем, если бы мы выбрали эти функции. Чтобы инициализировать T ref параметр, инициализация списка будет использоваться, потому что это не прямая инициализация с помощью parens (поэтому инициализация параметра эквивалентна T ref t = { 1 }). Первые две функции являются точными совпадениями. Однако в Стандарте говорится, что в таком случае, когда одна функция преобразуется в std::initializer_list<T> а другой нет, тогда выигрывает первая функция. Поэтому в этом сценарии вторая ctor будет использоваться. Обратите внимание, что в этом случае мы не будем выполнять двухфазное разрешение перегрузки с первым списком инициализаторов ctors - только инициализация списка будет делать это ,


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

struct T {
  T(long);
  T(std::initializer_list<int>);
};

T t = { 1L };

В этом случае мы делаем двухфазное разрешение перегрузки , Сначала мы рассмотрим только конструкторы списка инициализаторов и посмотрим, соответствует ли один из них, в качестве аргумента мы берем полный список инициализации. Второй ctor соответствует, поэтому мы выбираем его. Мы проигнорируем первый конструктор. Если у нас нет списка инициализаторов ctor или если его нет, мы берем все ctors и элементы списка инициализаторов

struct T {
  T(long);

  template<typename A = std::initializer_list<int>>
  T(A);
};

T t = { 1L };

В этом случае мы выбираем первый конструктор, потому что 1L не могут быть преобразованы в std::initializer_list<int>,


2



В приведенном выше примере std :: vector является просто заполнителем, меня интересует общий ответ.

Как «общий» ответ вы хотите? Потому что это означает действительно  зависит от того, какой тип инициализации вы используете и какие у них есть конструкторы.

Например:

T a{ 2, 3, 5, 7};
T b( { 2, 3, 5, 7} );

Эти май  быть двумя разными вещами. Или они не могут. Это зависит от того, какие конструкторы Tесть. Если T имеет конструктор, который принимает один initializer_list<int> (или некоторые другие initializer_list<U>, где U является интегральным типом), то оба они назовут этот конструктор.

Однако, если это не так, то эти два будут делать разные вещи. Во-первых, попытается вызвать конструктор, который принимает 4 аргумента, которые могут быть сгенерированы целыми литералами. Второй будет пытаться вызвать конструктор, который принимает один аргумент , который он попытается инициализировать с помощью {2, 3, 5, 7}, Это означает, что он будет проходить через каждый конструктор с одним аргументом, выяснить, какой тип для этого аргумента, и попытаться построить его с помощью R{2, 3, 5, 7} Если никто из них не работает, он попытается передать его как initializer_list<int>, И если это не сработает, то это не сработает.

initializer_list конструкторы всегда имеют приоритет.

Обратите внимание, что initializer_list конструкторы только в игре, потому что {2, 3, 5, 7} является скобкой-init-list, где каждый элемент имеет тот же тип. Если у тебя есть {2, 3, 5.3, 7.9}, то он не будет проверять initializer_list Конструкторы.

T c = { 2, 3, 5, 7};

Это будет вести себя как a, за исключением того, какие преобразования он будет делать. Поскольку это инициализация списка копий, он попытается вызвать конструктор initializer_list. Если такой конструктор недоступен, он попытается вызвать конструктор с 4 аргументами, но это позволит неявные преобразования  его аргументов в параметры типа.

Это единственное различие. Он не требует конструкторов копирования / перемещения или чего-либо (спецификация только упоминает инициализацию списка копий в 3-х местах. Ни одна из них не запрещает ее, когда копирование / перемещение не доступно). Это почти точно эквивалентно a за исключением вида преобразования, которое он допускает по своим аргументам.

Вот почему его обычно называют «равномерной инициализацией»: потому что он работает почти одинаково везде.


2



Традиционно (C ++ 98/03), инициализация T x(T()); вызывается прямая инициализация и инициализация T x = T(); вызывается инициализация копирования. Когда вы использовали инициализацию копирования, копия ctor должна была быть доступна, даже если она не могла (т. Е. Обычно не использовалась).

Инициализатор перечисляет вид изменений, которые. Глядя на §8.5 / 14 и §8.5 / 15, видно, что условия прямая инициализация  а также копия инициализация  все еще применяются, но, смотря на §8.5 / 16, мы обнаруживаем, что для расширенного списка инициализации это различие без разницы, по крайней мере для ваших первых и третьих примеров:

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

Таким образом, фактическая инициализация для вашего первого и третьего примеров выполняется одинаково, и не требуется копия ctor (или перемещение ctor). В обоих случаях мы имеем дело с четвертой пулей в § 8.5.4 / 3:

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

... поэтому оба используют std::vector, который принимает std::initializer_list<T> как его аргумент.

Однако, как указано выше в цитате, это относится только к «скобке-init-list» (без скобок). Для вашего второго примера с скобками-скобками-скобками в скобках мы попадаем в первую суб-пулю шестой пули (geeze - действительно нужно поговорить с кем-то о добавлении чисел для них) из § 8.5 / 16:

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

Так как это использует синтаксис для прямой инициализации, а выражение внутри круглых скобок представляет собой список с бисером-инициализатором и std::vector имеет ctor, который принимает список инициализаторов, это выбранная перегрузка.

Итог: хотя маршруты по стандарту, чтобы попасть туда, разные, все трое в конечном итоге используют std::vectorперегрузка конструктора для std::initializer_list<T>, С любой практической точки зрения нет разницы между этими тремя. Все три будут ссылаться vector::vector(std::initializer_list<T>, без каких-либо копий или других преобразований (даже те, которые могут быть исключены и действительно происходят только в теории).

Я считаю, что при немного разных значениях, однако, существует (или, по крайней мере, может быть) одно незначительное различие. Запрет на сужение конверсий приведен в п. 8.5.4 / 3, поэтому ваш второй пример (который, если можно так выразиться, в § 8.5.4 / 3) должен, вероятно, разрешить сужение конверсий, где другие два явно не делают этого. Однако, даже если бы я был заядлым игроком, я бы не стал спорить с компилятором, который действительно признавал это различие и позволял сужать преобразование в одном случае, но не в других (я нахожу это немного удивительным и, скорее, сомневаюсь, что это предназначенный для разрешения).


2



Я немного поиграл в gcc 4.7.2 с использованием пользовательского класса std::initializer_list в конструкторе. Я пробовал все эти сценарии и многое другое. Кажется, на самом деле нет никакой разницы в наблюдаемых результатах этого компилятора для этих трех операторов.

РЕДАКТИРОВАТЬ:  Это точный код, который я использовал для тестирования:

#include <iostream>
#include <initializer_list>

class A {
public:
  A()                    { std::cout << "A::ctr\n"; }
  A(const A&)            { std::cout << "A::ctr_copy\n"; }
  A(A&&)                 { std::cout << "A::ctr_move\n"; }
  A &operator=(const A&) { std::cout << "A::=_copy\n"; return *this; }
  A &operator=(A&&)      { std::cout << "A::=_move\n"; return *this; }
  ~A()                   { std::cout << "A::dstr\n"; }
};

class B {
  B(const B&)            { std::cout << "B::ctr_copy\n"; }
  B(B&&)                 { std::cout << "B::ctr_move\n"; }
  B &operator=(const B&) { std::cout << "B::=copy\n"; return *this; }
  B &operator=(B&&)      { std::cout << "B::=move\n"; return *this; }
public:
  B(std::initializer_list<A> init) { std::cout << "B::ctr_ user\n"; }
  ~B()                             { std::cout << "B::dstr\n"; }
};

int main()
{
  B a1{ {}, {}, {} };
  B a2({ {}, {}, {} });
  B a3 = { {}, {}, {} };
  // B a4 = B{ {}, {}, {} }; // does not compile on gcc 4.7.2, gcc 4.8 and clang (top version)
  std::cout << "--------------------\n";
}

a1, a2 а также a3 компилирует штраф на gcc 4.7.2, gcc 4.8 и последний clang. Я также не вижу никаких наблюдаемых результатов между количеством операций, выполняемых членами списка, для всех трех случаев. Последний случай (а не вопрос) не компилируется, если я делаю B copy / move constructor private / deleted.


1