Wie emuliert man das C-Array-Initialisierungsverhalten „int arr [] = {e1, e2, e3,…}“ mit std :: array?


137

(Hinweis: Bei dieser Frage geht es darum, die Anzahl der Elemente nicht angeben zu müssen und dennoch zu ermöglichen, dass verschachtelte Typen direkt initialisiert werden.) In
dieser Frage werden die Verwendungszwecke für ein C-Array wie erläutert int arr[20];. In seiner Antwort zeigt @James Kanze eine der letzten Hochburgen von C-Arrays, seine einzigartigen Initialisierungseigenschaften:

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

Wir müssen die Anzahl der Elemente nicht angeben, hurra! Durchlaufen Sie es jetzt mit den C ++ 11-Funktionen std::beginund std::endvon <iterator>( oder Ihren eigenen Varianten ), und Sie müssen nicht einmal an seine Größe denken.

Gibt es nun (möglicherweise TMP) Möglichkeiten, dasselbe zu erreichen std::array? Die Verwendung von Makros erlaubt es, es schöner aussehen zu lassen. :) :)

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

Bearbeiten : Zwischenversion, zusammengestellt aus verschiedenen Antworten, sieht folgendermaßen aus:

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

Und verwendet alle Arten von coolen C ++ 11-Sachen:

  • Variadische Vorlagen
  • sizeof...
  • rWertreferenzen
  • perfekte Weiterleitung
  • std::array, Na sicher
  • einheitliche Initialisierung
  • Weglassen des Rückgabetyps mit einheitlicher Initialisierung
  • Typinferenz ( auto)

Ein Beispiel finden Sie hier .

Wie @Johannes im Kommentar zur Antwort von @ Xaade ausführt, können Sie verschachtelte Typen mit einer solchen Funktion jedoch nicht initialisieren. Beispiel:

struct A{ int a; int b; };

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

Außerdem ist die Anzahl der Initialisierer auf die Anzahl der von der Implementierung unterstützten Funktions- und Vorlagenargumente beschränkt.


Variadische Methode. Es ist keine Initialisierung, eher eine Zuweisung, aber es ist die nächste, zu der ich kommen kann. Um die Initialisierung zu erhalten, müssen Sie direkten Zugriff auf den Speicher haben.
Lee Louviere

Anscheinend unterstützt C ++ 0x die Initialisierersyntax. Genial. Es ist, als würde man mehr wie C # sein, mit Sprachunterstützung für kompliziertere Unterstützung. Weiß jemand, ob wir formale Sprachunterstützung für Schnittstellen bekommen ???
Lee Louviere

10
@ Downvoter: Grund?
Xeo

1
Entschuldigung, was bedeutet TMPIhre Frage?
Kevinarpe

1
@kevinarpe TMP steht wahrscheinlich für Template Metaprogramming .
BeeOnRope

Antworten:


63

Das Beste, was ich mir vorstellen kann, ist:

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

Dies erfordert jedoch, dass der Compiler NRVO ausführt und dann auch die Kopie des zurückgegebenen Werts überspringt (was ebenfalls legal, aber nicht erforderlich ist). In der Praxis würde ich erwarten, dass jeder C ++ - Compiler dies so optimieren kann, dass es so schnell wie eine direkte Initialisierung ist.


gcc 4.6.0 lässt den zweiten nicht kompilieren und beschwert sich über die Einschränkung der Konvertierung von double in value_type, aber clang ++ 2.9 ist für beide in Ordnung!
Cubbi

20
Mit solchen Antworten verstehe ich am meisten, was Bjarne über das Gefühl "wie eine neue Sprache" gesagt hat :) Variadische Vorlagen, Spätrückgabespezifizierer und Typabzug in einem!
Matthieu M.

@Matthieu: Fügen Sie jetzt rvalue refs, perfekte Weiterleitung und einheitliche Initialisierung aus @ DeadMGs Code hinzu, und Sie haben viele neue Funktionen. :>
Xeo

1
@Cubbi: Eigentlich ist g ++ genau hier - einschränkende Konvertierungen sind bei der aggregierten Initialisierung in C ++ 0x nicht zulässig (aber in C ++ 03 zulässig - eine wichtige Änderung, die mir nicht bekannt war!). Ich werde den zweiten make_arrayAnruf entfernen .
Pavel Minaev

@Cubbi, ja, aber das ist eine explizite Konvertierung - es würde auch stille Downcasts und andere solche Dinge erlauben. Dies kann immer noch mit static_assertund etwas TMP erfolgen, um zu erkennen, wann Tailnicht implizit konvertierbar Tist und dann verwendet wird T(tail)..., aber das bleibt übrig als Übung für den Leser :)
Pavel Minaev

39

Ich würde eine einfache erwarten 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
Entfernen Sie das std::array<ret, sizeof...(T)>auf der returnAnweisung. Dies zwingt einen Verschiebungskonstruktor für den Array-Typ sinnlos dazu, T&&in C ++ 14 und C ++ 11 zu existieren (im Gegensatz zu einem Konstrukt aus ).
Yakk - Adam Nevraumont

7
Ich liebe es, wie C ++ Leute das einfach nennen :-)
Ciro Santilli 郝海东 冠状 病 六四 六四 26

20

Hier ist eine Lösung, die einige Ideen aus früheren Beiträgen kombiniert und auch für verschachtelte Konstruktionen funktioniert (getestet in 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)...};
}

Seltsamerweise kann der Rückgabewert nicht zu einer r-Wert-Referenz gemacht werden, die für verschachtelte Konstruktionen nicht funktioniert. Wie auch immer, hier ist ein 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]]]

(Für die letzte Ausgabe verwende ich meinen hübschen Drucker .)


Lassen Sie uns die Typensicherheit dieser Konstruktion verbessern. Wir brauchen definitiv alle Typen, um gleich zu sein. Eine Möglichkeit besteht darin, eine statische Zusicherung hinzuzufügen, die ich oben bearbeitet habe. Die andere Möglichkeit besteht darin, nur zu aktivieren, make_arraywenn die Typen gleich sind, wie folgt :

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

In jedem Fall benötigen Sie das Merkmal vom all_same<Args...>Typ Variadic . Hier ist sie , verallgemeinert aus std::is_same<S, T>(beachten Sie, dass abklingende wichtig ist , damit die Vermischung T, T&, T const &etc.):

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

Beachten Sie, dass die make_array()Rückgabe per Kopie des Temporären erfolgt, die der Compiler (mit ausreichenden Optimierungsflags!) Als rWert behandeln oder auf andere Weise optimieren darf, undstd::array es sich um einen Aggregattyp handelt, sodass der Compiler die bestmögliche Konstruktionsmethode auswählen kann .

Beachten Sie schließlich, dass Sie die Kopier- / Verschiebungskonstruktion beim make_arrayEinrichten des Initialisierers nicht vermeiden können . Hat std::array<Foo,2> x{Foo(1), Foo(2)};also keine Kopie / Verschiebung, sondern auto x = make_array(Foo(1), Foo(2));zwei Kopien / Verschiebungen, an die die Argumente weitergeleitet werden make_array. Ich glaube nicht, dass Sie das verbessern können, weil Sie eine Liste mit variablen Initialisierern nicht lexikalisch an den Helfer übergeben und Typ und Größe ableiten können - wenn der Präprozessor eine sizeof...Funktion für verschiedene Argumente hätte, könnte dies möglicherweise getan werden, aber nicht innerhalb der Kernsprache.


13

Die Verwendung der Trailing-Return-Syntax make_arraykann weiter vereinfacht werden

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

Leider erfordert es für Aggregatklassen eine explizite Typspezifikation

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

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

Tatsächlich ist diese make_arrayImplementierung im Operator sizeof ... aufgeführt


c ++ 17 Version

Dank des Abzugs von Vorlagenargumenten für den Vorschlag für Klassenvorlagen können wir Abzugsleitfäden verwenden, um den make_arrayHelfer loszuwerden

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

Kompiliert mit -std=c++1zFlag unter x86-64 gcc 7.0


6
C ++ 17 sollte bereits einen Abzugsleitfaden
underscore_d

6

Ich weiß, dass es einige Zeit her ist, seit diese Frage gestellt wurde, aber ich bin der Meinung, dass die vorhandenen Antworten immer noch einige Mängel aufweisen, daher möchte ich meine leicht modifizierte Version vorschlagen. Im Folgenden sind die Punkte aufgeführt, von denen ich denke, dass einige vorhandene Antworten fehlen.


1. Sie müssen sich nicht auf RVO verlassen

Einige Antworten erwähnen, dass wir uns auf RVO verlassen müssen, um das Konstruierte zurückzugeben array. Das ist nicht wahr; Wir können die Initialisierung der Kopierliste verwenden , um sicherzustellen , dass niemals temporäre Elemente erstellt werden. Also statt:

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

wir sollten tun:

return {{values}};

2. Machen Sie make_arrayeine constexprFunktion

Auf diese Weise können wir konstante Arrays zur Kompilierungszeit erstellen.

3. Sie müssen nicht überprüfen, ob alle Argumente vom gleichen Typ sind

Wenn dies nicht der Fall ist, gibt der Compiler ohnehin eine Warnung oder einen Fehler aus, da die Listeninitialisierung keine Einschränkung zulässt. Zweitens sollten wir, selbst wenn wir uns wirklich dazu entschließen, unser eigenes static_assertDing zu machen (vielleicht um eine bessere Fehlermeldung zu liefern), wahrscheinlich eher die verfallenen Typen der Argumente als die rohen Typen vergleichen. Beispielsweise,

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

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

Wenn wir einfach sind , static_assertdass ing a, bund chaben die gleiche Art, dann wird diese Überprüfung fehlschlagen, aber das ist wahrscheinlich nicht das, was würden wir erwarten. Stattdessen sollten wir ihre std::decay_t<T>Typen vergleichen (die alle ints sind).

4. Leiten Sie den Array-Werttyp ab, indem Sie die weitergeleiteten Argumente ablehnen

Dies ähnelt Punkt 3. Verwenden Sie dasselbe Code-Snippet, geben Sie diesmal jedoch den Werttyp nicht explizit an:

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

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

Wir wollen wahrscheinlich eine machen array<int, 3>, aber die Implementierungen in den vorhandenen Antworten scheitern wahrscheinlich alle daran. Was wir tun können, ist, anstatt a zurückzugeben std::array<T, …>, a zurückzugeben std::array<std::decay_t<T>, …>.

Dieser Ansatz hat einen Nachteil: Wir können keinen arraycv-qualifizierten Werttyp mehr zurückgeben. Aber die meiste Zeit, anstatt so etwas wie ein array<const int, …>, würden wir const array<int, …>sowieso ein verwenden. Es gibt einen Kompromiss, aber ich denke einen vernünftigen. Das C ++ 17 verfolgt std::make_optionalauch diesen Ansatz:

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

Unter Berücksichtigung der oben genannten Punkte make_arraysieht eine voll funktionsfähige Implementierung von in C ++ 14 folgendermaßen aus:

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

Verwendung:

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

6

C ++ 11 unterstützt diese Art der Initialisierung für (die meisten?) Standardcontainer.


1
Ich denke jedoch, dass OP die Größe des Arrays nicht angeben möchte, aber size ist ein Vorlagenparameter von std :: array. Sie benötigen also so etwas wie std :: array <unsigned int, 5> n = {1,2,3,4,5};
Juanchopanza

std::vector<>benötigt keine explizite Ganzzahl, und ich bin mir nicht sicher, warum std::array.
Richard

@Richard, weil std :: vector eine dynamische Größe hat und std :: array eine feste Größe hat. Siehe dies: en.wikipedia.org/wiki/Array_(C%2B%2B)
Juanchopanza

@juanchopanza, aber die {...}Syntax impliziert eine konstante Ausdehnung zur Kompilierungszeit, sodass der ctor in der Lage sein sollte, die Ausdehnung abzuleiten.
Richard

1
std::initializer_list::sizeist keine constexprFunktion und kann daher nicht so verwendet werden. Es gibt jedoch Pläne von libstdc ++ (der Implementierung, die mit GCC ausgeliefert wird), ihre Version zu haben constexpr.
Luc Danton

5

(Lösung von @dyp)

Hinweis: erfordert C ++ 14 ( std::index_sequence). Obwohl man std::index_sequencein C ++ 11 implementieren könnte .

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

Ich habe die Standardinitialisierung der std :: array-Elemente übersehen. Derzeit auf der Suche nach einem Fix.
Gabriel Garcia

@dyp Ich habe die Antwort mit Ihrem Code aktualisiert. Wenn Sie sich dazu entschließen, Ihre eigene Antwort aufzuschreiben, lassen Sie es mich wissen und ich werde meine stürzen. Danke dir.
Gabriel Garcia

1
Nein, alles in Ordnung. Das Binden eines temporären Arrays, um die Länge abzuleiten, ist Ihre Idee, und ich habe nicht überprüft, ob mein Code überhaupt kompiliert wird. Ich denke, es ist immer noch Ihre Lösung und Antwort mit etwas Verfeinerung;) Man könnte jedoch argumentieren, dass eine Variadik keinen Nutzen hat, make_arraywie in Puppys Antwort.
Dyp

Richtig. Darüber hinaus können Vorlagen keine Typen aus Initialisierungslisten ableiten, was eine der Anforderungen der Frage ist (verschachtelte Klammerinitialisierung).
Gabriel Garcia

1

17 ++ 17 kompakte Implementierung.

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


0

Erstellen Sie einen Array-Maker-Typ.

Es wird überladen operator,, um eine Ausdrucksvorlage zu generieren, die jedes Element über Referenzen mit dem vorherigen verkettet.

Füge hinzu ein finish freie Funktion hinzu, die den Array-Hersteller übernimmt und ein Array direkt aus der Referenzkette generiert.

Die Syntax sollte ungefähr so ​​aussehen:

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

Es erlaubt keine {}basierte Konstruktion, wie nur operator=. Wenn Sie bereit sind zu verwenden =, können wir es zum Laufen bringen:

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

oder

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

Keine dieser Lösungen scheint gut zu sein.

Durch die Verwendung von Variardics wird die vom Compiler festgelegte Anzahl von Varargs und Blöcken auf die rekursive Verwendung {}für Unterstrukturen begrenzt.

Am Ende gibt es wirklich keine gute Lösung.

Was ich tue, ist, dass ich meinen Code so schreibe, dass er sowohl T[]als auch std::arrayDaten unabhängig voneinander verbraucht - es ist mir egal, welchen ich ihn füttere. Manchmal bedeutet dies, dass mein Weiterleitungscode []Arrays sorgfältig std::arraytransparent in s umwandeln muss .


1
"Das sieht nicht nach guten Lösungen aus." Ist das, was ich auch sagen würde: p
Kappen
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.