Варианты 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 параметр, инициализация списка будет использоваться, потому что это не прямая инициализация с паренами (поэтому инициализация параметра эквивалентна T ref t = { 1 }). Первые две функции являются точными совпадениями. Однако в стандарте говорится, что в таком случае, когда одна функция преобразуется в std::initializer_list<T> а другой нет, тогда первая функция побеждает. Поэтому в этом сценарии второй ctor будет использоваться. Обратите внимание, что в этом сценарии мы не будем выполнять двухфазное разрешение перегрузки с первыми только инициаторами списка инициализаторов — это будет делать только инициализация списка.


Для первых двух мы будем использовать инициализацию списка, и она будет делать контекстно-зависимые вещи. Если 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} это список фигурных скобок, где каждый элемент имеет одинаковый тип. Если у тебя есть {2, 3, 5.3, 7.9}то не проверял initializer_list Конструкторы.

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

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

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

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

2

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

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

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

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

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

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

Однако, как отмечено в приведенной выше цитате, это касается только «(не заключенного в скобки) фигурного списка инициализации». Для вашего второго примера с заключенным в скобки фигурным списком инициализации мы переходим к первому подпункту шестой маркировки (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 в конструкторе. Я перепробовал все эти сценарии и многое другое. Кажется, на самом деле нет никакой разницы в наблюдаемых результатах этого компилятора для этих 3 операторов.

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

#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 конструктор копирования / перемещения приватный / удаленный.

1
По вопросам рекламы [email protected]