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


138

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

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

Нам не нужно указывать количество элементов, ура! Теперь перебирайте его с помощью функций C ++ 11 std::beginи std::endfrom <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);

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

  • Вариативные шаблоны
  • 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} };

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


Вариативный метод. Это не инициализация, скорее присваивание, но это самое близкое, к чему я могу подойти. Для инициализации у вас должен быть прямой доступ к памяти.
Ли Лувьер,

По-видимому, C ++ 0x поддерживает синтаксис инициализатора. Потрясающие. Это похоже на то, чтобы стать больше похожим на C #, с языковой поддержкой для более сложной поддержки. Кто-нибудь знает, получим ли мы формальную языковую поддержку интерфейсов ???
Ли Лувьер

10
@Downvoter: Причина?
Xeo 02

1
Извините, что означает TMPваш вопрос?
kevinarpe

1
@kevinarpe TMP, вероятно, означает метапрограммирование шаблонов .
BeeOnRope

Ответы:


63

Лучшее, что я могу придумать, это:

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, а затем также пропустил копию возвращаемого значения (что также допустимо, но не обязательно). На практике я ожидал бы, что любой компилятор C ++ сможет оптимизировать это так, чтобы это было так же быстро, как прямая инициализация.


gcc 4.6.0 не позволяет компилировать второй, жалуясь на сужение преобразования от double к value_type, но clang ++ 2.9 подходит для обоих!
Cubbi

21
Именно с такими ответами я лучше всего понимаю, что Бьярн сказал о чувстве «нового языка» :) Шаблоны с переменным числом аргументов, спецификатор позднего возврата и вывод типа - все в одном!
Matthieu M.

@Matthieu: Теперь добавьте ссылки rvalue, идеальную пересылку и единообразную инициализацию из кода @ DeadMG, и у вас есть множество новых функций. :>
Xeo

1
@Cubbi: на самом деле здесь g ++ - сужающие преобразования не разрешены при агрегированной инициализации в C ++ 0x (но разрешены в C ++ 03 - критическое изменение, о котором я не знал!). Сниму второй make_arrayзвонок.
Павел Минаев

@Cubbi, да, но это явное преобразование - оно также разрешает тихие понижающие передачи и другие подобные вещи. Это все еще можно сделать, используя static_assertи некоторый TMP, чтобы определить, когда Tailнеявно преобразован в T, а затем использовать T(tail)..., но это осталось в качестве упражнения для читателя :)
Павел Минаев

39

Я ожидал простого 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)... };
}

1
Удалите значок std::array<ret, sizeof...(T)>на returnзаявлении. Это бессмысленно заставляет существовать конструктор перемещения для типа массива (в отличие от конструкции из - T&&) в C ++ 14 и C ++ 11.
Якк - Адам Неврамонт

9
Мне нравится, как люди C ++ называют это простым :-)
Чиро Сантилли 冠状 病 六四 事件 法轮功

20

Объединив несколько идей из предыдущих сообщений, вот решение, которое работает даже для вложенных конструкций (протестировано в 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)...};
}

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


14

Использование синтаксиса конечного возврата 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 ...


версия c ++ 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


6
В C ++ 17 уже должно быть руководство по дедукции для этого: en.cppreference.com/w/cpp/container/array/deduction_guides
underscore_d

6

C ++ 11 будет поддерживать этот способ инициализации для (большинства?) Контейнеров std.


1
Однако я думаю, что OP не хочет указывать размер массива, но размер является параметром шаблона std :: array. Итак, вам нужно что-то вроде std :: array <unsigned int, 5> n = {1,2,3,4,5};
juanchopanza

std::vector<>не нужно явное целое число, и я не уверен, зачем std::array.
Ричард

@Richard, потому что std :: vector имеет динамический размер, а std :: array имеет фиксированный размер. См. Это: en.wikipedia.org/wiki/Array_(C%2B%2B)
juanchopanza

@juanchopanza, но {...}синтаксис подразумевает постоянную степень времени компиляции, поэтому ctor должен иметь возможность определить степень.
Ричард

1
std::initializer_list::sizeне является constexprфункцией и поэтому не может использоваться в таком виде. Однако есть планы из libstdc ++ (реализация, поставляемая с GCC) иметь свою версию constexpr.
Люк Дантон

6

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


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

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

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>типы (все они ints)).

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, …>вернуть вместо возврата a std::array<std::decay_t<T>, …>.

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

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

Принимая во внимание вышесказанное, полная рабочая реализация make_arrayна C ++ 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>, 0> make_array() noexcept
{
    return {};
}

Применение:

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

5

(Решение от @dyp)

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

#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;
}

Я пропустил инициализацию по умолчанию элементов std :: array. Сейчас ищу исправление.
Габриэль Гарсия

@dyp Я обновил ответ вашим кодом. Если вы решите написать свой собственный ответ, дайте мне знать, и я опишу свой. Спасибо.
Габриэль Гарсия

1
Нет, все хорошо. Связывание временного массива для определения длины - это ваша идея, и я не проверял, компилируется ли мой код. Я думаю, что это все еще ваше решение, и ответ с некоторой доработкой;) Однако можно возразить, что нет никакой пользы от вариативного, make_arrayкак в ответе Puppy.
dyp

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

1

Компактная реализация С ++ 17.

template <typename... T>
constexpr auto array_of(T&&... t) {
    return std::array{ static_cast<std::common_type_t<T...>>(t)... };
}

0

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


этот подходит. и аналогичный вопрос об использовании его для назначения вложенных структур можно найти здесь. Использование-assign-map-list-of-for-complex-types
Ассамбар 01

0

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

Он перегружается 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}] );

Ни одно из этих решений не выглядит хорошим.

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

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

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


1
«Это не похоже на хорошие решения». Я бы тоже сказал: p
caps
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.