Почему список инициализаторов можно использовать только при объявлении?

Массивы могут быть инициализированы так называемым списком инициализации.

Например:

int my_array[3] = {10, 20, 30};

Это очень полезно, когда у нас есть набор начальных значений для нашего массива. Однако этот подход не работает для назначения новых значений массиву после его объявления.

my_array = {10, 20, 30};

error: assigning to an array from an initializer list

Однако иногда у нас есть процессы, в которых нам нужно несколько раз инициализировать наши массивы для некоторых начальных значений (например, внутри цикла), поэтому я думаю, что было бы очень полезно иметь возможность использовать списки инициализаторов для назначения значений уже объявленным переменным.

Мой вопрос: есть ли причина для того, чтобы иметь такую функцию во время объявления, но не после того, как массив объявлен? Почему это работает в одном случае, но не в другом?

Ответы

Ответ 1

Массивы являются гражданами второго сорта в C++. Это объекты, но они строго ограничены: их нельзя копировать, они разлагаются на указатели в различных контекстах и т.д. Рассмотрите возможность использования std::array, который является оболочкой (фиксированного размера) поверх встроенных массивов, но это первоклассный гражданин, который поддерживает различные функции удобства:

std::array<int, 3> my_array = {10, 20, 30};
my_array = {40, 50, 60};

Это работает, потому что, согласно [array.overview]/2,

std::array является агрегатным типом, который может быть инициализирован списком максимум с N элементами, типы которых могут быть преобразованы в T

live demo

Это также работает с std::vector. Векторы - это отдельная история, поэтому я не буду вдаваться в подробности.


Если вы предпочитаете настаивать на встроенных массивах, вот обходной путь, который я разработал, чтобы позволить присваивать список значений встроенному массиву (с учетом категорий значений), используя методы метапрограммирования шаблонов. Ошибка времени компиляции (правильно) возникает, если длина массива и список значений не совпадают. (Спасибо комментарию Калет за это!) Обратите внимание, что копирование встроенных массивов невозможно в C++; вот почему мы должны передать массив в функцию.

namespace detail {
  template <typename T, std::size_t N, std::size_t... Ints, typename... Args>
  void assign_helper(T (&arr)[N], std::index_sequence<Ints...>, Args&&... args)
  {
    ((arr[Ints] = args), ...);
  }
}

template <typename T, std::size_t N, typename... Args>
void assign(T (&arr)[N], Args&&... args)
{
  return detail::assign_helper(arr, std::make_index_sequence<N>{}, std::forward<Args>(args)...);
}

И использовать это:

int arr[3] = {10, 20, 30};
assign(arr, 40, 50, 60);

Теперь arr состоит из 40, 50, 60.

live demo

Ответ 2

Массивы могут быть инициализированы так называемым списком инициализации.

Ну нет.

Класс может быть инициализирован с помощью списка инициализации, он должен иметь конструктор, который принимает std::initializer_list.

Пример:

vector( std::initializer_list<T> init, const Allocator& alloc = Allocator());

Массивы не являются классами, поэтому у них не может быть конструктора. Но они могут быть инициализированы с помощью агрегатной инициализации:

Агрегат является одним из следующих типов:

  • тип массива
  • ...

И, как сказал Л.Ф., они не могут быть скопированы:

присваивание

Объекты типа массива нельзя изменить в целом: даже если они являются l-значениями (например, адрес массива может быть взят), они не могут появляться в левой части оператора присваивания.

Источник: https://en.cppreference.com/w/cpp/language/array

Вот почему синтаксис {} работает для инициализации, а не для присваивания, потому что это не означает то же самое.

Ответ 3

Есть ли причина наличия такой возможности во время объявления, но не после объявления массива? Почему это работает в одном случае, но не в другом?

Синтаксис x = {a, b,...} включает определенный тип списков инициализаторов, называемый copy-list-initialization. В cppreference упоминаются возможные способы использования инициализации списка копирования:

  • T object = {arg1, arg2,...}; (6)
  • function( { arg1, arg2,... } ) (7)
  • return { arg1, arg2,... } ; (8)
  • object[ { arg1, arg2,... } ] (9)
  • object = { arg1, arg2,... } (10)
  • U( { arg1, arg2,... } ) (11)
  • Class { T member = { arg1, arg2,... }; }; (12)

Синтаксис массива, который вы попробовали T myArr[] = {a, b, c...} работает и нумеруется как (6) инициализация именованной переменной с помощью фигурного списка инициализации после знака равенства.

Синтаксис, который не работает для вас (myArr = {a, b,...}), нумеруется как (10) и называется инициализацией списка в выражении присваивания. Особенность выражений присваивания заключается в том, что левая сторона должна быть так называемым lvalue, и, хотя массивы являются lvalue, они не могут появляться в левой части присваиваний согласно спецификации.


При этом было бы не сложно обойти присваивание, скопировав список инициализатора в массив как таковой:

#include <algorithm>
#include <iostream>

int main() {
  int arr[] = {1, 2, 3};

  auto itr = arr;
  auto assign = {5, 2, 1};
  std::copy(assign.begin(), assign.end(), itr);
}