Як емуляцію ініціалізації масиву C "int arr [] = {e1, e2, e3, ...}" з поведінкою std :: array?


137

(Примітка. Це питання стосується того, що не потрібно вказувати кількість елементів і все ж дозволяти вкладеним типам безпосередньо ініціалізувати.)
У цьому питанні розглядаються питання, які залишилися для масиву С, як 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::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);

І використовує всі цікаві матеріали 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

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 не дозволяє компілювати другий, скаржившись на звуження перетворення з подвійного в value_type, але clang ++ 2.9 в порядку з обома!
Cubbi

21
Саме з такими відповідями я найбільше розумію те, що сказав Б’ярн про почуття "як нової мови" :) Варіанти шаблонів, специфікатор пізнього повернення та вирахування типу "все в одному"!
Матьє М.

@Matthieu: Тепер додайте rvalue refs, ідеальну переадресацію та рівномірну ініціалізацію з коду @ 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.
Якк - Адам Невраумон

8
Мені подобається, як люди на C ++ називають так просто :-)
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

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()повернення тимчасово копіюється, яке компілятору (з достатніми прапорами оптимізації!) Дозволено розглядати як рецензію або іншим чином оптимізувати, і std::arrayце агрегатний тип, тому компілятор вільний вибрати найкращий можливий спосіб побудови .

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


13

Використання синтаксису зворотного зворотного зв'язку 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реалізація занесена до розміру оператора ...


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
підкреслити

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>типи (які є всіint ).

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>, але реалізація в існуючих відповідях, мабуть, не вдається цього зробити. Що ми можемо зробити, це замість повернення a 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>_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, "!");

6

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


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

std::vector<>не потрібно явного цілого числа, і я не впевнений, чому std::arrayб це зробити.
Річард

@Richard, тому що std :: vector має динамічний розмір, а std :: масив має фіксований розмір. Дивіться це: en.wikipedia.org/wiki/Array_(C%2B%2B)
juanchopanza

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

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

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як у відповіді Щеня, немає користі .
dyp

Правильно. Більше того, шаблони не можуть виводити типи зі списків ініціалізаторів, що є однією з вимог запитання (вкладеної braced ініціалізації).
Габріель Гарсія

1

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

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

0

Якщо std :: масив не є обмеженням, і якщо у вас є Boost, погляньте на це list_of(). Це не зовсім як ініціалізація масиву типу C, яку ви хочете. Але близько.


це добре. та подібне запитання щодо його використання для призначення вкладених структур можна знайти тут Використання-призначити-карту-список-для-складних типів
Ассамбар

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}] );

Жодне з них не схоже на хороші рішення.

Використання variardics обмежує обмеження, накладене вами компілятором, на кількість реєрсивних використання варагів та блоків {} для підструктур.

Зрештою, насправді немає хорошого рішення.

Те, що я роблю, - це те, що я пишу свій код, щоб він споживав як дані, так T[]і std::arrayдані агностично - не важливо, яким я його готую. Іноді це означає, що мій код переадресації повинен обережно перетворювати []масиви в std::arrays прозоро.


1
"Вони не виглядають як хороші рішення". Це те , що я хотів би сказати, теж: р
ковпачків
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.