Jak emulować inicjalizację tablicy w C zachowanie „int arr [] = {e1, e2, e3,…}” za pomocą std :: array?

137

(Uwaga: to pytanie dotyczy braku konieczności określania liczby elementów i nadal zezwala na bezpośrednie inicjowanie typów zagnieżdżonych).
To pytanie omawia zastosowania pozostawione dla tablicy C, takiej jak int arr[20];. W swojej odpowiedzi @James Kanze pokazuje jeden z ostatnich bastionów tablic C, jego unikalne cechy inicjalizacji:

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

Nie musimy określać liczby elementów, hura! Teraz iteruj go za pomocą funkcji C ++ 11 std::begini std::endfrom <iterator>( lub własnych wariantów ) i nigdy nie musisz nawet myśleć o jego rozmiarze.

Czy są jakieś sposoby (prawdopodobnie TMP), aby osiągnąć to samo std::array? Zastosowanie makr pozwoliło uatrakcyjnić wygląd. :)

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

Edycja : wersja pośrednia, skompilowana z różnych odpowiedzi, wygląda następująco:

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

I używa wszelkiego rodzaju fajnych rzeczy C ++ 11:

  • Szablony Variadic
  • sizeof...
  • odniesienia do wartości r
  • doskonała spedycja
  • std::array, oczywiście
  • jednolita inicjalizacja
  • pomijanie zwracanego typu przy jednolitej inicjalizacji
  • wnioskowanie o typie ( auto)

Przykład można znaleźć tutaj .

Jednak , jak @Johannes wskazuje w komentarzu do odpowiedzi @ Xaade, nie możesz zainicjować zagnieżdżonych typów za pomocą takiej funkcji. Przykład:

struct A{ int a; int b; };

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

Ponadto liczba inicjatorów jest ograniczona do liczby argumentów funkcji i szablonów obsługiwanych przez implementację.

Xeo
źródło
Metoda wariadyczna. To nie jest inicjalizacja, bardziej jak przypisanie, ale jest najbliższa, do której mogę dojść. Aby uzyskać inicjalizację, musiałbyś mieć bezpośredni dostęp do pamięci.
Lee Louviere
Najwyraźniej C ++ 0x obsługuje składnię inicjalizatora. Niesamowite. To tak, jakbyś stał się bardziej podobny do C #, z obsługą języków dla bardziej skomplikowanej obsługi. Czy ktoś wie, czy otrzymamy formalne wsparcie językowe dla interfejsów ???
Lee Louviere
10
@Downvoter: Powód?
Xeo
1
Przepraszam, jakie jest znaczenie TMPtwojego pytania?
kevinarpe
1
@kevinarpe TMP prawdopodobnie oznacza metaprogramowanie szablonów .
BeeOnRope

Odpowiedzi:

63

Najlepsze co przychodzi mi do głowy to:

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

Wymaga to jednak od kompilatora wykonania NRVO, a następnie pominięcia kopii zwracanej wartości (co również jest legalne, ale nie jest wymagane). W praktyce spodziewałbym się, że każdy kompilator C ++ będzie w stanie zoptymalizować to tak, aby był tak szybki, jak bezpośrednia inicjalizacja.

Pavel Minaev
źródło
gcc 4.6.0 nie pozwala na kompilację drugiego, narzekając na zawężenie konwersji z double do value_type, ale clang ++ 2.9 jest OK z obydwoma!
Cubbi
21
Dzięki takim odpowiedziom najbardziej rozumiem, co powiedział Bjarne o poczuciu się „jak nowy język” :) Szablony zmienne, specyfikator późnego zwrotu i odliczanie typów - wszystko w jednym!
Matthieu M.
@Matthieu: Teraz dodaj rvalue refs, doskonałe przekazywanie i jednolitą inicjalizację z kodu @ DeadMG i masz zestaw wielu nowych funkcji. :>
Xeo
1
@Cubbi: w rzeczywistości g ++ jest tutaj - zawężające konwersje nie są dozwolone w inicjalizacji zbiorczej w C ++ 0x (ale dozwolone w C ++ 03 - przełomowa zmiana, o której nie byłem świadomy!). Usunę drugie make_arraypołączenie.
Pavel Minaev
@Cubbi, tak, ale to jest jawna konwersja - pozwoliłaby również na ciche spadki i inne tego typu rzeczy.Nadal można to zrobić za pomocą static_asserti niektórych TMP do wykrywania, kiedy Tailnie jest niejawnie konwertowane na T, a następnie użycie T(tail)..., ale to pozostaje jako ćwiczenie dla czytelnika :)
Pavel Minaev
39

Spodziewałbym się prostego 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)... };
}
Szczeniak
źródło
1
Usuń znak std::array<ret, sizeof...(T)>na returnwyciągu. To bezcelowo wymusza istnienie konstruktora przenoszenia w typie tablicy (w przeciwieństwie do konstrukcji-z- T&&) w C ++ 14 i C ++ 11.
Yakk - Adam Nevraumont,
8
Uwielbiam to, jak ludzie C ++ nazywają to prostym :-)
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
20

Łącząc kilka pomysłów z poprzednich postów, oto rozwiązanie, które działa nawet w przypadku konstrukcji zagnieżdżonych (przetestowane w 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)...};
}

O dziwo, nie można uczynić zwracanej wartości referencją rvalue, która nie działałaby w przypadku konstrukcji zagnieżdżonych. Tak czy inaczej, oto test:

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]]]

(Do ostatniego wydruku używam mojej ładnej drukarki .)


Właściwie poprawmy bezpieczeństwo typowe tej konstrukcji. Zdecydowanie potrzebujemy, aby wszystkie typy były takie same. Jednym ze sposobów jest dodanie statycznej asercji, którą zredagowałem powyżej. Innym sposobem jest włączenie tylko make_arraywtedy, gdy typy są takie same, na przykład:

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

Tak czy inaczej, będziesz potrzebować all_same<Args...>cechy typu wariadycznego. Oto ona, uogólniając od std::is_same<S, T>(zauważ, że rozpadają ważne jest, aby umożliwić mieszanie T, T&, T const &itd.):

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

Zwróć uwagę, że make_array()zwraca przez copy-of-tymczasowe, które kompilator (z wystarczającymi flagami optymalizacji!) Może traktować jako wartość r lub w inny sposób optymalizować, istd::array jest typem zagregowanym, więc kompilator może wybrać najlepszą możliwą metodę konstrukcji .

Na koniec zwróć uwagę, że nie możesz uniknąć konstrukcji kopiuj / przenieś podczas make_arraykonfigurowania inicjatora. Więc std::array<Foo,2> x{Foo(1), Foo(2)};nie ma kopiowania / przenoszenia, ale auto x = make_array(Foo(1), Foo(2));ma dwa kopiowanie / przenoszenie, ponieważ argumenty są przekazywane make_array. Nie sądzę, aby można było to poprawić, ponieważ nie można przekazać leksykalnie zmiennej listy inicjalizatora do pomocnika i wywnioskować typ i rozmiar - gdyby preprocesor miał sizeof...funkcję dla argumentów wariadycznych, być może można to zrobić, ale nie w rdzennym języku.

Kerrek SB
źródło
13

Używanie składni końcowego powrotu make_arraymożna jeszcze bardziej uprościć

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

Niestety w przypadku klas zagregowanych wymaga to jawnej specyfikacji typu

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

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

W rzeczywistości ta make_arrayimplementacja jest wymieniona w sizeof ... operator


Wersja c ++ 17

Dzięki dedukcji argumentów szablonu dla propozycji szablonów klas możemy użyć przewodników dedukcyjnych, aby pozbyć się make_arraypomocnika

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

Skompilowane z -std=c++1zflagą pod x86-64 gcc 7.0

wyczyszczone
źródło
6
C ++ 17 powinien już mieć przewodnik po dedukcji: en.cppreference.com/w/cpp/container/array/deduction_guides
underscore_d
6

Wiem, że minęło sporo czasu, odkąd zadano to pytanie, ale czuję, że istniejące odpowiedzi nadal mają pewne wady, dlatego chciałbym zaproponować moją nieco zmodyfikowaną wersję. Poniżej znajdują się punkty, których, jak sądzę, brakuje niektórych istniejących odpowiedzi.


1. Nie musisz polegać na RVO

Niektóre odpowiedzi wspominają, że musimy polegać na RVO, aby zwrócić skonstruowany array. To nie jest prawda; możemy skorzystać z inicjalizacji listy kopii, aby zagwarantować, że nigdy nie zostaną utworzone tymczasowe. Więc zamiast:

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

Powinniśmy to zrobić:

return {{values}};

2. Producentmake_arrayconstexpr funkcję

To pozwala nam tworzyć tablice stałych czasu kompilacji.

3. Nie ma potrzeby sprawdzania, czy wszystkie argumenty są tego samego typu

Po pierwsze, jeśli tak nie jest, kompilator i tak wygeneruje ostrzeżenie lub błąd, ponieważ inicjalizacja listy nie pozwala na zawężenie. Po drugie, nawet jeśli naprawdę zdecydujemy się zrobić własne static_assertrzeczy (być może w celu zapewnienia lepszego komunikatu o błędzie), nadal powinniśmy prawdopodobnie porównywać zepsute typy argumentów, a nie typy surowe. Na przykład,

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

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

Jeśli jesteśmy po prostu static_asserting że a, bi cmają ten sam typ, to sprawdzenie nie powiedzie się, ale to chyba nie jest to, czego oczekujemy. Zamiast tego powinniśmy porównać ich std::decay_t<T>typy (czyli wszystkieint s)).

4. Zmniejsz typ wartości tablicy, rozkładając przekazane argumenty

Jest to podobne do punktu 3. Używając tego samego fragmentu kodu, ale tym razem nie określaj jawnie typu wartości:

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

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

Prawdopodobnie chcemy to zrobić array<int, 3>, ale implementacje w istniejących odpowiedziach prawdopodobnie nie są w stanie tego zrobić. Zamiast zwracać a std::array<T, …>, możemy zwrócić astd::array<std::decay_t<T>, …> .

Jest jedna wada tego podejścia: nie możemy już zwracać arraytypu wartości kwalifikowanej jako cv. Ale przez większość czasu zamiast czegoś takiego jak an array<const int, …>, i const array<int, …>tak używalibyśmy a . Jest kompromis, ale myślę, że jest rozsądny. C ++ 17 std::make_optionalrównież przyjmuje to podejście:

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

Biorąc pod uwagę powyższe punkty, pełna działająca implementacja make_arrayw C ++ 14 wygląda następująco:

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

Stosowanie:

constexpr auto arr = make_array(make_array(1, 2),
                                make_array(3, 4));
static_assert(arr[1][1] == 4, "!");
Zizheng Tai
źródło
6

C ++ 11 będzie obsługiwał ten sposób inicjalizacji dla (większości?) Standardowych kontenerów.

Richard
źródło
1
Jednak myślę, że OP nie chce określać rozmiaru tablicy, ale rozmiar jest parametrem szablonu std :: array. Potrzebujesz więc czegoś takiego jak std :: array <unsigned int, 5> n = {1,2,3,4,5};
juanchopanza
std::vector<>nie potrzebuje jawnej liczby całkowitej i nie jestem pewien, dlaczego std::arraymiałby to robić .
Richard
@Richard, ponieważ std :: vector ma rozmiar dynamiczny, a std :: array ma stały rozmiar. Zobacz: en.wikipedia.org/wiki/Array_(C%2B%2B)
juanchopanza
@juanchopanza, ale {...}składnia implikuje stały zakres czasu kompilacji, więc ctor powinien być w stanie wydedukować zakres.
Richard
1
std::initializer_list::sizenie jest constexprfunkcją i dlatego nie może być używany w ten sposób. Istnieją jednak plany z libstdc ++ (implementacja dostarczana z GCC), aby mieć swoją wersję constexpr.
Luc Danton
5

(Rozwiązanie autorstwa @dyp)

Uwaga: wymaga C ++ 14 ( std::index_sequence). Chociaż można by zaimplementować std::index_sequencew 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;
}
Gabriel Garcia
źródło
Przeoczyłem domyślną inicjalizację elementów std :: array. Obecnie szukam poprawki.
Gabriel Garcia
@dyp Zaktualizowałem odpowiedź Twoim kodem. Jeśli zdecydujesz się napisać własną odpowiedź, daj mi znać, a ja obniżę swoją. Dziękuję Ci.
Gabriel Garcia
1
Nie, w porządku. Wiązanie tymczasowej tablicy w celu ustalenia długości to twój pomysł, a ja nie sprawdzałem, czy mój kod w ogóle się kompiluje. Myślę, że to wciąż twoje rozwiązanie i odpowiedź, z pewnym dopracowaniem;) Można by się jednak spierać, że nie ma żadnej korzyści z wariadyków, make_arrayjak w odpowiedzi Puppy.
dyp
Dobrze. Ponadto szablony nie mogą wywnioskować typów z list inicjalizujących, co jest jednym z wymagań pytania (zagnieżdżona inicjalizacja stężona).
Gabriel Garcia
1

С ++ 17 kompaktowa implementacja.

template <typename... T>
constexpr auto array_of(T&&... t) {
    return std::array{ static_cast<std::common_type_t<T...>>(t)... };
}
Piotr
źródło
0

Jeśli std :: array nie jest ograniczeniem i jeśli masz Boost, spójrz na list_of(). Nie jest to dokładnie takie samo, jak inicjalizacja tablicy typu C, której chcesz. Ale blisko.

yasouser
źródło
to jest dobre. a podobne pytanie dotyczące używania go do przypisywania zagnieżdżonych struktur można znaleźć tutaj Using-assign-map-list-of-for-complex-types
Assambar
0

Utwórz typ kreatora tablic.

Przeciąża, operator,aby wygenerować szablon wyrażenia łączący każdy element z poprzednim za pomocą odwołań.

Dodaj finishbezpłatną funkcję, która pobiera narzędzie do tworzenia tablic i generuje tablicę bezpośrednio z łańcucha odniesień.

Składnia powinna wyglądać mniej więcej tak:

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

Nie zezwala {}na budowę opartą na konstrukcji, jak tylko to operator=robi. Jeśli chcesz skorzystać =, możemy sprawić, że zadziała:

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

lub

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

Żadne z tych rozwiązań nie wygląda na dobre.

Korzystanie z varardics ogranicza cię do narzuconego przez kompilator limitu liczby varargów i blokuje rekurencyjne użycie {} dla podstruktur.

Ostatecznie nie ma dobrego rozwiązania.

Co mogę zrobić, to piszę mojego kodu tak zużywa zarówno T[]i std::arraydane agnostically - to nie obchodzi, które karmię go. Czasami oznacza to, że mój kod przekazujący musi ostrożnie przekształcić []tablice w std::arrayprzezroczyste.

Yakk - Adam Nevraumont
źródło
1
„To nie wygląda na dobre rozwiązania”. Jest to, co chciałbym powiedzieć, zbyt: p
czapki