Как эмулировать инициализацию массива C "int arr [] = {e1, e2, e3,...}" поведение с std:: array?

(Примечание. Этот вопрос связан с необходимостью не указывать количество элементов и разрешать прямую инициализацию вложенных типов.)
В этом вопросе обсуждается использование слева для массива C, например int arr[20];. На его ответ, @James Kanze показывает один из последних оплотов массивов C, это уникальные характеристики инициализации:

int arr[] = { 1, 3, 3, 7, 0, 4, 2, 0, 3, 1, 4, 1, 5, 9 };

Нам не нужно указывать количество элементов, ура! Теперь перебираем его с помощью функций С++ 11 std::begin и std::end из <iterator> (или ваших собственных вариантов), и вам никогда не нужно даже думать о его размер.

Теперь, есть ли какие-либо (возможно, TMP) способы достичь того же с помощью std::array? Использование макросов позволило сделать его более приятным.:)

??? std_array = { "here", "be", "elements" };

Изменить. Промежуточная версия, скомпилированная из различных ответов, выглядит следующим образом:

#include <array>
#include <utility>

template<class T, class... Tail, class Elem = typename std::decay<T>::type>
std::array<Elem,1+sizeof...(Tail)> make_array(T&& head, Tail&&... values)
{
  return { std::forward<T>(head), std::forward<Tail>(values)... };
}

// in code
auto std_array = make_array(1,2,3,4,5);

И использует все классные С++ 11 вещи:

  • Шаблоны Variadic
  • sizeof...
  • ссылки rvalue
  • совершенная переадресация
  • std::array, конечно
  • равномерная инициализация
  • опускание возвращаемого типа с равномерной инициализацией
  • вывод типа (auto)

И пример можно найти здесь.

Однако, как указывает @Johannes в комментарии к ответу @Xaade, вы не можете инициализировать вложенные типы с помощью такой функции. Пример:

struct A{ int a; int b; };

// C syntax
A arr[] = { {1,2}, {3,4} };
// using std::array
??? std_array = { {1,2}, {3,4} };

Кроме того, количество инициализаторов ограничено количеством аргументов функции и шаблона, поддерживаемых реализацией.

Ответ 1

Лучше всего я могу подумать:

template<class T, class... Tail>
auto make_array(T head, Tail... tail) -> std::array<T, 1 + sizeof...(Tail)>
{
     std::array<T, 1 + sizeof...(Tail)> a = { head, tail ... };
     return a;
}

auto a = make_array(1, 2, 3);

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

Ответ 2

Я бы ожидал простой make_array.

template<typename ret, typename... T> std::array<ret, sizeof...(T)> make_array(T&&... refs) {
    // return std::array<ret, sizeof...(T)>{ { std::forward<T>(refs)... } };
    return { std::forward<T>(refs)... };
}

Ответ 3

Объединив несколько идей из предыдущих сообщений, здесь решение, которое работает даже для вложенных конструкций (проверено в GCC4.6):

template <typename T, typename ...Args>
std::array<T, sizeof...(Args) + 1> make_array(T && t, Args &&... args)
{
  static_assert(all_same<T, Args...>::value, "make_array() requires all arguments to be of the same type."); // edited in
  return std::array<T, sizeof...(Args) + 1>{ std::forward<T>(t), std::forward<Args>(args)...};
}

Странно, can не может сделать возвращаемое значение ссылкой rvalue, которая не будет работать для вложенных конструкций. Во всяком случае, здесь тест:

auto q = make_array(make_array(make_array(std::string("Cat1"), std::string("Dog1")), make_array(std::string("Mouse1"), std::string("Rat1"))),
                    make_array(make_array(std::string("Cat2"), std::string("Dog2")), make_array(std::string("Mouse2"), std::string("Rat2"))),
                    make_array(make_array(std::string("Cat3"), std::string("Dog3")), make_array(std::string("Mouse3"), std::string("Rat3"))),
                    make_array(make_array(std::string("Cat4"), std::string("Dog4")), make_array(std::string("Mouse4"), std::string("Rat4")))
                    );

std::cout << q << std::endl;
// produces: [[[Cat1, Dog1], [Mouse1, Rat1]], [[Cat2, Dog2], [Mouse2, Rat2]], [[Cat3, Dog3], [Mouse3, Rat3]], [[Cat4, Dog4], [Mouse4, Rat4]]]

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


Собственно, давайте улучшим безопасность типа этой конструкции. Нам определенно нужно, чтобы все типы были одинаковыми. Один из способов - добавить статическое утверждение, которое я редактировал выше. Другим способом является включение только make_array, когда типы одинаковы, например:

template <typename T, typename ...Args>
typename std::enable_if<all_same<T, Args...>::value, std::array<T, sizeof...(Args) + 1>>::type
make_array(T && t, Args &&... args)
{
  return std::array<T, sizeof...(Args) + 1> { std::forward<T>(t), std::forward<Args>(args)...};
}

В любом случае вам понадобится переменная all_same<Args...>. Вот он, обобщающий из std::is_same<S, T> (обратите внимание, что разложение важно для смешивания T, T&, T const & и т.д.):

template <typename ...Args> struct all_same { static const bool value = false; };
template <typename S, typename T, typename ...Args> struct all_same<S, T, Args...>
{
  static const bool value = std::is_same<typename std::decay<S>::type, typename std::decay<T>::type>::value && all_same<T, Args...>::value;
};
template <typename S, typename T> struct all_same<S, T>
{
  static const bool value = std::is_same<typename std::decay<S>::type, typename std::decay<T>::type>::value;
};
template <typename T> struct all_same<T> { static const bool value = true; };

Обратите внимание, что make_array() возвращается с помощью copy-of-tempor, который компилятору (с достаточными флажками оптимизации!) разрешено обрабатывать как rvalue или иначе оптимизировать, а std::array является агрегированным типом, поэтому компилятор может выбрать наилучший возможный метод строительства.

Наконец, обратите внимание, что вы не можете избежать копирования/перемещения, когда make_array устанавливает инициализатор. Таким образом, std::array<Foo,2> x{Foo(1), Foo(2)}; не имеет копии/перемещения, но auto x = make_array(Foo(1), Foo(2)); имеет две копии/перемещения, поскольку аргументы перенаправляются на make_array. Я не думаю, что вы можете улучшить это, потому что вы не можете передавать вариационный список инициализаторов лексически в помощник и выводить тип и размер - если препроцессор имел функцию sizeof... для вариативных аргументов, возможно, это можно было бы сделать, но не в пределах основного языка.

Ответ 4

Тот факт, что std:: array не может быть инициализирован как C-массив, низок. Я сделал сообщение об этом на comp.lang.С++. Модерировал несколько месяцев назад, где было предложено решение make_array, как здесь.

Ссылка для заинтересованных лиц: http://groups.google.com/group/comp.lang.c++.moderated/browse_thread/thread/f1a5a1451b003bec

Функции, такие как make_array, хорошо и хорошо, но вам не нужно использовать сложную функцию для инициализации чего-то фундаментального, как массив. Это еще одна область, которая дает С++ плохую репутацию. Если язык не может получить инициализацию массива справа, это показывает тяжелую ситуацию imo.

Примеры, как показано ниже, должны работать! Нет больше глупостей, пожалуйста!

std::array <int> arr = { 1, 3, 3, 7, 0, 4, 2, 0, 3, 1, 4, 1, 5, 9 }; //automatically deduces its size from the initializer list :)

Ответ 5

Использование синтаксиса обратной ссылки make_array может быть дополнительно упрощено

#include <array>
#include <type_traits>
#include <utility>

template <typename... T>
auto make_array(T&&... t)
  -> std::array<std::common_type_t<T...>, sizeof...(t)>
{
  return {std::forward<T>(t)...};
}

int main()
{
  auto arr = make_array(1, 2, 3, 4, 5);
  return 0;
}

К сожалению, для агрегатных классов требуется явная спецификация типа

/*
struct Foo
{
  int a, b;
}; */

auto arr = make_array(Foo{1, 2}, Foo{3, 4}, Foo{5, 6});

Фактически эта реализация make_array указана в sizeof... operator


С++ 17 версия

Благодаря предложение аргумента шаблона для шаблонов классов мы можем использовать инструкции для вычитания, чтобы избавиться от make_array хелпера

#include <array>

namespace std
{
template <typename... T> array(T... t)
  -> array<std::common_type_t<T...>, sizeof...(t)>;
}

int main()
{
  std::array a{1, 2, 3, 4};
  return 0; 
}

Скомпилирован с флагом -std=c++1z под x86-64 gcc 7.0

Ответ 7

Я знаю, что прошло довольно много времени с тех пор, как был задан этот вопрос, но я чувствую, что в существующих ответах все еще есть некоторые недостатки, поэтому я хотел бы предложить свою слегка измененную версию. Ниже приведены пункты, которые я думаю, что некоторые существующие ответы отсутствуют.


1. Не нужно полагаться на RVO

В некоторых ответах упоминается, что нам нужно полагаться на RVO для возврата построенного array. Это неправда; мы можем использовать copy-list-initialization, чтобы гарантировать, что временные файлы никогда не будут созданы. Так что вместо:

return std::array<Type, …>{values};

мы должны сделать:

return {{values}};

2. Сделать make_array в constexpr функцию

Это позволяет нам создавать постоянные массивы во время компиляции.

3. Нет необходимости проверять, что все аргументы имеют одинаковый тип

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

volatile int a = 0;
const int& b = 1;
int&& c = 2;

auto arr = make_array<int>(a, b, c);  // Will this work?

Если мы просто static_assert что a, b и c имеют одинаковый тип, то эта проверка завершится неудачно, но, вероятно, это не то, что мы ожидали. Вместо этого мы должны сравнить их std::decay_t<T> (которые все являются int s)).

4. Выведите тип значения массива, убрав переданные аргументы

Это похоже на пункт 3. Использование того же фрагмента кода, но на этот раз не указывайте тип значения явно:

volatile int a = 0;
const int& b = 1;
int&& c = 2;

auto arr = make_array(a, b, c);  // Will this work?

Мы, вероятно, хотим создать array<int, 3>, но реализации в существующих ответах, вероятно, не смогут этого сделать. Что мы можем сделать, это вместо того, чтобы возвращать std::array<T, …>, возвращать std::array<std::decay_t<T>, …>.

У этого подхода есть один недостаток: мы больше не можем возвращать array значений с квалификацией cv. Но в большинстве случаев вместо чего-то вроде array<const int, …> мы все равно использовали бы const array<int, …>. Есть компромисс, но я думаю, что разумный. С++ 17 std::make_optional также использует этот подход:

template< class T > 
constexpr std::optional<std::decay_t<T>> make_optional( T&& value );

Принимая во внимание вышеизложенное, полная рабочая реализация make_array в С++ 14 выглядит следующим образом:

#include <array>
#include <type_traits>
#include <utility>

template<typename T, typename... Ts>
constexpr std::array<std::decay_t<T>, 1 + sizeof... (Ts)>
make_array(T&& t, Ts&&... ts)
    noexcept(noexcept(std::is_nothrow_constructible<
                std::array<std::decay_t<T>, 1 + sizeof... (Ts)>, T&&, Ts&&...
             >::value))

{
    return {{std::forward<T>(t), std::forward<Ts>(ts)...}};
}

template<typename T>
constexpr std::array<std::decay<T>_t, 0> make_array() noexcept
{
    return {};
}

Использование:

constexpr auto arr = make_array(make_array(1, 2),
                                make_array(3, 4));
static_assert(arr[1][1] == 4, "!");

Ответ 8

(Решение @dyp)

Примечание: требуется С++ 14 (std::index_sequence). Хотя в С++ 11 можно реализовать std::index_sequence.

#include <iostream>

// ---

#include <array>
#include <utility>

template <typename T>
using c_array = T[];

template<typename T, size_t N, size_t... Indices>
constexpr auto make_array(T (&&src)[N], std::index_sequence<Indices...>) {
    return std::array<T, N>{{ std::move(src[Indices])... }};
}

template<typename T, size_t N>
constexpr auto make_array(T (&&src)[N]) {
    return make_array(std::move(src), std::make_index_sequence<N>{});
}

// ---

struct Point { int x, y; };

std::ostream& operator<< (std::ostream& os, const Point& p) {
    return os << "(" << p.x << "," << p.y << ")";
}

int main() {
    auto xs = make_array(c_array<Point>{{1,2}, {3,4}, {5,6}, {7,8}});

    for (auto&& x : xs) {
        std::cout << x << std::endl;
    }

    return 0;
}

Ответ 9

Это еще не решение. Массивы стиля C объединяют два важные функции: статическая инициализация и автоматический подсчет инициализаторы. Вероятно, существуют решения с использованием вариационного шаблона аргументы, но инициализаторы (по крайней мере, в моем случае) часто строковые литералы, которые не являются приемлемыми аргументами для шаблона; и большинство времени, это массив из struct; типичным случаем было бы что-то вроде:

struct Table
{
    char const* key;
    int value;
};
static Table const table[] = { ... };

У меня есть шаблон класса для определения этих типов структур; в дополнение к элементам данных, он создает функциональный объект-член для соответствия и статической функции-члена find, которая ее использует. Но шаблон класса по-прежнему не определяет таблицу; всего лишь один элемент. Итак, вы в конечном итоге пишете что-то вроде:

StaticMap<int> const table[] =
{
    { "first key", 1 },
    { "second key", 2 },
    //  ...
};

и позже:

StaticMap<int> const* entry = StaticMap<int>::find( key );

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

Ответ 10

Если std:: array не является ограничением, и если у вас есть Boost, взгляните на list_of(). Это не похоже на инициализацию массива типа C, которую вы хотите. Но близко.

Ответ 11

Создайте тип создателя массива.

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

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

Синтаксис должен выглядеть примерно так:

auto arr = finish( make_array<T>->* 1,2,3,4,5 );

Это не разрешает построение на основе {}, как это делает только operator=. Если вы хотите использовать =, мы можем заставить его работать:

auto arr = finish( make_array<T>= {1}={2}={3}={4}={5} );

или

auto arr = finish( make_array<T>[{1}][{2}[]{3}][{4}][{5}] );

Ни один из них не выглядит как хорошие решения.

Использование variardics ограничивает ваше ограничение на количество переменных varargs и блоков рекурсивным использованием {} для подструктур.

В конце концов, действительно не очень хорошее решение.

То, что я делаю, это написать свой код, поэтому он потребляет как T[], так и std::array данные агностически - мне все равно, что я его кормлю. Иногда это означает, что мой код пересылки должен тщательно преобразовывать массивы [] в std::array.