Объявление и инициализация массива в C++11

Вот 8 способов объявить и инициализировать массивы в C++11, которые подходят для g++:

/*0*/ std::array<int, 3> arr0({1, 2, 3});
/*1*/ std::array<int, 3> arr1({{1, 2, 3}});
/*2*/ std::array<int, 3> arr2{1, 2, 3};
/*3*/ std::array<int, 3> arr3{{1, 2, 3}};
/*4*/ std::array<int, 3> arr4 = {1, 2, 3};
/*5*/ std::array<int, 3> arr5 = {{1, 2, 3}};
/*6*/ std::array<int, 3> arr6 = std::array<int, 3>({1, 2, 3});
/*7*/ std::array<int, 3> arr7 = std::array<int, 3>({{1, 2, 3}});

Каковы правильные в соответствии со строгим стандартом (и будущим стандартом С++ 14)? Какие из них наиболее распространены/используются, а какие следует избегать (и по какой причине)?


person Vincent    schedule 11.12.2013    source источник
comment
При запросе g++ делайте это с помощью -Wall -pedantic. Сделайте то же самое для Clang, это очень помогает. Например, в arr 4 отсутствует вторая пара {}.   -  person usr1234567    schedule 11.12.2013
comment
Во-первых, я задавался вопросом, почему arr0 и arr1 вообще работают; они называют движение-ctor? Если да, то они имеют другое значение, чем 2-5 (аналогично 6 и 7).   -  person dyp    schedule 11.12.2013
comment
Почему откат? Номера версий были полезны и не отвлекали.   -  person    schedule 07.06.2015
comment
@dudeprgm в верхнем ответе говорится, что примеры 0,2,6 не требуются для работы. Однако из вашей нумерации неясно, какие это строки. Без вашего редактирования я бы предположил, что пример 0 означает arr0, поэтому ваша нумерация добавила путаницы (во всяком случае, для меня)   -  person M.M    schedule 07.06.2015
comment
@MattMcNabb ... ой, извините, я откатился к более ранней версии кого-то, у которого номера начинались с примера 1, а не с примера 0. Теперь это должно быть исправлено.   -  person    schedule 07.06.2015


Ответы (4)


Сводка C++11 / TL;DR

  • Из-за дефекта удаления фигурной скобки примеры 0, 2, 6 не требуются для работы. Однако последние версии компиляторов реализуют предлагаемое решение для этого дефекта, так что эти примеры будут работать.
  • Поскольку не указано, содержит ли std::array необработанный массив. Поэтому примеры 1, 3, 5, 7 не обязательны для работы. Однако я не знаю реализации стандартной библиотеки, где они не работают (на практике).
  • Пример 4 всегда будет работать: std::array<int, 3> arr4 = {1, 2, 3};

Я бы предпочел версию 4 или версию 2 (с исправлением скобок), поскольку они инициализируются напрямую и требуются/вероятно будут работать.

Для стиля AAA Саттера вы можете использовать auto arrAAA = std::array<int, 3>{1, 2, 3};, но для этого требуется исправление исключения фигурных скобок.


std::array должен быть агрегатом [array.overview]/2, это означает, что у него нет пользовательских конструкторов (т. е. только по умолчанию, копирование, перемещение ctor).


std::array<int, 3> arr0({1, 2, 3});
std::array<int, 3> arr1({{1, 2, 3}});

Инициализация с помощью (..) является прямой инициализацией. Для этого требуется вызов конструктора. В случае arr0 и arr1 возможен только конструктор копирования/перемещения. Таким образом, эти два примера означают создание временного std::array из braced-init-list и копирование/перемещение его в место назначения. Благодаря исключению копирования/перемещения компилятор может исключить эту операцию копирования/перемещения, даже если она имеет побочные эффекты.

Н.Б. даже несмотря на то, что временные объекты являются значениями prvalue, он может вызвать копию (семантически, до исключения копии), поскольку ctor перемещения std::array может не быть объявлен неявно, например. если бы он был удален.


std::array<int, 3> arr6 = std::array<int, 3>({1, 2, 3});
std::array<int, 3> arr7 = std::array<int, 3>({{1, 2, 3}});

Это примеры инициализации копирования. Созданы два временных файла:

  • через braced-init-list {1, 2, 3} для вызова конструктора копирования/перемещения
  • через выражение std::array<int, 3>(..)

последний временный затем копируется/перемещается в указанную переменную назначения. Создание обоих временных объектов можно исключить.

Насколько мне известно, реализация может написать конструктор explicit array(array const&) = default; и не нарушать Стандарт; это сделало бы эти примеры неправильными. (Эта возможность исключена [container.requirements.general], слава Дэвиду Крауссу, см. это обсуждение.)


std::array<int, 3> arr2{1, 2, 3};
std::array<int, 3> arr3{{1, 2, 3}};
std::array<int, 3> arr4 = {1, 2, 3};
std::array<int, 3> arr5 = {{1, 2, 3}};

Это совокупная инициализация. Все они «напрямую» инициализируют std::array, без вызова конструктора std::array и без (семантического) создания временного массива. Члены std::array инициализируются с помощью инициализации копированием (см. ниже).


На тему брекет-элизион:

В стандарте C++11 удаление фигурных скобок применяется только к объявлениям формы T x = { a };, но не к T x { a };. Это считается дефектом и будет исправлено в C++1y, однако предлагаемое разрешение не является частью стандарта (статус DRWP, см. верхнюю часть связанной страницы), и поэтому вы не можете рассчитывать на то, что ваш компилятор реализует его также для T x { a };.

Следовательно, std::array<int, 3> arr2{1, 2, 3}; (примеры 0, 2, 6), строго говоря, некорректны. Насколько я знаю, последние версии clang++ и g++ уже допускают исключение скобок в T x { a };.

В примере 6 std::array<int, 3>({1, 2, 3}) использует инициализацию копированием: инициализация для передачи аргументов также является инициализацией копированием. Однако ошибочное ограничение пропуска фигурных скобок, "В объявлении формы T x = { a };", также запрещает исключение скобок для передачи аргументов, поскольку это не объявление и, конечно, не в такой форме.


По теме агрегатной инициализации:

Как указывает Йоханнес Шауб, в комментарии только гарантируется, что вы можете инициализировать std::array со следующим синтаксисом [массив.обзор]/2:

array<T, N> a = { initializer-list };

Из этого можно сделать вывод, что, если исключение фигурных скобок разрешено в форме T x { a };, что синтаксис

array<T, N> a { initializer-list };

правильно сформирован и имеет тот же смысл. Однако не гарантируется, что std::array фактически содержит необработанный массив в качестве единственного элемента данных (см. также LWG 2310). Я думаю, что одним из примеров может быть частичная специализация std::array<T, 2>, где есть два члена данных T m0 и T m1. Следовательно, нельзя сделать вывод, что

array<T, N> a {{ initializer-list }};

хорошо сформирован. К сожалению, это приводит к тому, что не существует гарантированного способа инициализации временного исключения std::array без скобок для T x { a };, а также означает, что нечетные примеры (1, 3, 5, 7) не требуются для работы.


Все эти способы инициализации std::array в конечном итоге приводят к агрегатной инициализации. Он определяется как копирование-инициализация элементов агрегата. Однако инициализация копированием с использованием списка инициализации в фигурных скобках может по-прежнему напрямую инициализировать составной элемент. Например:

struct foo { foo(int); foo(foo const&)=delete; };
std::array<foo, 2> arr0 = {1, 2};      // error: deleted copy-ctor
std::array<foo, 2> arr1 = {{1}, {2}};  // error/ill-formed, cannot initialize a
                                       // possible member array from {1}
                                       // (and too many initializers)
std::array<foo, 2> arr2 = {{{1}, {2}}}; // not guaranteed to work

Первый пытается инициализировать элементы массива из предложений инициализатора 1 и 2 соответственно. Эта копия-инициализация эквивалентна foo arr0_0 = 1;, которая, в свою очередь, эквивалентна foo arr0_0 = foo(1);, что является незаконным (удален копировал-ctor).

Второй содержит не список выражений, а список инициализаторов, поэтому он не соответствует требованиям [array.overview]/2. На практике std::array содержит необработанный элемент данных массива, который будет инициализирован (только) из первого предложения инициализатора {1}, тогда как второе предложение {2} является недопустимым.

У третьего есть проблема, противоположная второму: он работает, если есть есть элемент данных массива, но это не гарантируется.

person dyp    schedule 11.12.2013
comment
Я бы проголосовал, если бы вы указали, что примеры 1, 3, 5 и 7 не гарантируют работу (поскольку они основаны на внутренней структуре/глубине агрегации). - person Johannes Schaub - litb; 17.12.2013
comment
@JohannesSchaub-litb Ммм... да... это плохо. Я реструктурировал свой ответ и включил свое понимание вашего комментария (что приводит к двум неудачным выводам). Вы случайно не знаете, есть ли какие-либо реализации, которые не используют необработанный элемент массива? - person dyp; 17.12.2013

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

Сочетание круглых скобок и фигурных скобок (0 и 1) меня никогда не устраивало, равенства (4 и 5) — нормально, но я просто предпочитаю более короткую версию, а 6 и 7 просто абсурдно многословны.

Однако вы можете пойти другим путем, следуя Стиль Херба Саттера "почти всегда авто":

auto arr8 = std::array<int, 3>{{1, 2, 3}};
person Sebastian Redl    schedule 11.12.2013
comment
Я пишу низкоуровневую библиотеку с большим количеством шаблонов, поэтому я максимально избегаю ключевого слова auto, потому что мне нужно отслеживать типы (но я согласен, что в большинстве случаев это может быть решением). - person Vincent; 11.12.2013
comment
auto, используемый таким образом, выглядит как написание кода Python, в котором тип объявленной переменной известен назначенному объекту. Не так уж плохо использовать его в этом примере, но я предпочитаю третий конструктор из начального поста. В основном я использую auto для итераторов, объявленных в циклах. - person lucas92; 11.12.2013
comment
Ответ @dyp, по-видимому, говорит, что std::array<int, 3>{{1, 2, 3}}; не гарантирует работу, потому что не указано, использует ли std::array массив C-стиля внутри (и, следовательно, {1,2,3} может неправильно инициализировать то, что он использует внутри) - person M.M; 07.06.2015

Этот ответ ссылается на отчет об ошибке, в котором -Wmissing-braces больше не включается по умолчанию при использовании -Wall. Если вы включите -Wmissing-braces, gcc будет жаловаться на 0, 2, 4 и 6 (так же, как clang).

Исключение скобок допускается для операторов в форме T a = { ... }, но не T a { }.

Почему поведение initializer_list C++ для std::vector и std::array разные?

Вот ответ Джеймса Макнеллиса:

Однако эти дополнительные фигурные скобки могут быть опущены только «в объявлении формы T x = { a };» (C++11 §8.5.1/11), то есть когда используется старый стиль =. Это правило, разрешающее удаление фигурных скобок, не применяется для прямой инициализации списка. Сноска здесь гласит: «Квадратные скобки нельзя опускать при других применениях инициализации списка».

Существует отчет об ошибке, касающийся этого ограничения: CWG defect< /а> #1270. Если предложенная резолюция будет принята, исключение фигурных скобок будет разрешено для других форм инициализации списка,...

Если предложенная резолюция будет принята, исключение фигурных скобок будет разрешено для других форм инициализации списка, и следующее будет правильным: std::array y{ 1, 2, 3, 4 };

И ответ Xeo:

... в то время как std::array не имеет конструкторов, а список инициализации в фигурных скобках {1, 2, 3, 4} на самом деле не интерпретируется как std::initializer_list, а совокупная инициализация для внутреннего массива C-стиля std ::array (отсюда второй набор фигурных скобок: один для std::array, один для внутреннего массива членов в стиле C).

std::array не имеет конструктора, который принимает initializer_list. По этой причине он рассматривается как агрегатная инициализация. Если бы это было так, это выглядело бы примерно так:

#include <array>
#include <initializer_list>

struct test {
    int inner[3];

    test(std::initializer_list<int> list) {
        std::copy(list.begin(), list.end(), inner);
    }
};

#include <iostream>

int main() { 
    test t{1, 2, 3};
    test t2({1, 2, 3});
    test t3 = {1, 2, 3};
    test t4 = test({1, 2, 3});
    for (int i = 0; i < 3; i++)
        std::cout << t.inner[i];
    for (int i = 0; i < 3; i++)
        std::cout << t2.inner[i];
    for (int i = 0; i < 3; i++)
        std::cout << t3.inner[i];
    for (int i = 0; i < 3; i++)
        std::cout << t4.inner[i];
}
person Community    schedule 11.12.2013

Последние 2 избыточны: вы можете использовать 6 форм объявления массива в правой части задания. Кроме того, если ваш компилятор не оптимизирует копию, эти версии менее эффективны.

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

person rems4e    schedule 11.12.2013