Come emulare l'inizializzazione dell'array C comportamento “int arr [] = {e1, e2, e3, ...}” con std :: array?


137

(Nota: questa domanda riguarda il non dover specificare il numero di elementi e consentire comunque l'inizializzazione diretta dei tipi nidificati.)
Questa domanda discute gli usi rimasti per un array C come int arr[20];. Sulla sua risposta , @James Kanze mostra una delle ultime roccaforti degli array C, le sue caratteristiche di inizializzazione uniche:

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

Non dobbiamo specificare il numero di elementi, evviva! Ora iteralo su di esso con le funzioni C ++ 11 std::begine std::endda <iterator>( o le tue varianti ) e non hai mai nemmeno bisogno di pensare alle sue dimensioni.

Ora, ci sono modi (possibilmente TMP) per ottenere lo stesso risultato std::array? L'uso di macro ha permesso di renderlo più bello. :)

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

Modifica : la versione intermedia, compilata da varie risposte, si presenta così:

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

E impiega tutti i tipi di cose interessanti in C ++ 11:

  • Modelli variabili
  • sizeof...
  • riferimenti di valore
  • spedizione perfetta
  • std::array, ovviamente
  • inizializzazione uniforme
  • omettendo il tipo restituito con inizializzazione uniforme
  • tipo inferenza ( auto)

E un esempio può essere trovato qui .

Tuttavia , come sottolinea @Johannes nel commento sulla risposta di @ Xaade, non è possibile inizializzare i tipi nidificati con tale funzione. Esempio:

struct A{ int a; int b; };

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

Inoltre, il numero di inizializzatori è limitato al numero di argomenti di funzione e modello supportati dall'implementazione.


Metodo variabile. Non è l'inizializzazione, più come un compito, ma è il più vicino a cui posso arrivare. Per ottenere l'inizializzazione, dovresti avere accesso diretto alla memoria.
Lee Louviere,

Apparentemente C ++ 0x supporta la sintassi dell'inizializzatore. Eccezionale. È come diventare più simili a C #, con il supporto linguistico per un supporto più complicato. Qualcuno sa se otteniamo il supporto linguistico formale per le interfacce ???
Lee Louviere,

10
@Downvoter: motivo?
Xeo

1
Scuse, qual è il significato della TMPtua domanda?
Kevin Augpe,

1
@kevinarpe TMP sta probabilmente per metaprogrammazione dei template .
BeeOnRope,

Risposte:


63

Il meglio che mi viene in mente è:

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

Tuttavia, ciò richiede che il compilatore esegua NRVO e quindi salti anche la copia del valore restituito (che è anche legale ma non obbligatorio). In pratica, mi aspetterei che qualsiasi compilatore C ++ sia in grado di ottimizzarlo in modo tale che sia veloce come l'inizializzazione diretta.


gcc 4.6.0 non consente la compilazione della seconda, lamentandosi di restringere la conversione da double a value_type, ma clang ++ 2.9 è OK con entrambe!
Cubbi,

21
È con risposte come questa che capisco di più quello che Bjarne ha detto riguardo al sentirsi "come una nuova lingua" :) Modelli variabili, identificatore di ritorno in ritardo e detrazione del tipo all-in-one!
Matthieu M.,

@Matthieu: ora aggiungi i riferimenti di valore, l'inoltro perfetto e l'inizializzazione uniforme dal codice di @ DeadMG e hai impostato molte nuove funzionalità. :>
Xeo

1
@Cubbi: in realtà, g ++ è proprio qui - il restringimento delle conversioni non è consentito nell'inizializzazione aggregata in C ++ 0x (ma consentito in C ++ 03 - un cambiamento di cui non ero a conoscenza!). Rimuoverò la seconda make_arraychiamata.
Pavel Minaev,

@Cubbi, sì, ma questa è una conversione esplicita - consentirebbe anche downcasts silenziosi e altre cose del genere. Questo può ancora essere fatto usando static_asserte alcuni TMP per rilevare quando Tailnon è implicitamente convertibile in T, e quindi usando T(tail)..., ma che rimane come esercizio per il lettore :)
Pavel Minaev,

39

Mi aspetterei un semplice 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
Rimuovi std::array<ret, sizeof...(T)>la returndichiarazione. Ciò costringe inutilmente un costruttore di mosse sul tipo di array (al contrario di un costrutto-da- T&&) in C ++ 14 e C ++ 11.
Yakk - Adam Nevraumont,

8
Adoro il modo in cui le persone C ++ lo chiamano così semplice :-)
Ciro Santilli 26 冠状 病 六四 事件 法轮功

20

Combinando alcune idee dei post precedenti, ecco una soluzione che funziona anche per costruzioni nidificate (testata 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)...};
}

Stranamente, non è possibile rendere il valore restituito un riferimento di valore, che non funzionerebbe per le costruzioni nidificate. Comunque, ecco un 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]]]

(Per l'ultimo output sto usando la mia pretty-printer .)


In realtà, miglioriamo la sicurezza del tipo di questa costruzione. Abbiamo sicuramente bisogno che tutti i tipi siano uguali. Un modo è aggiungere un'asserzione statica, che ho modificato sopra. L'altro modo è abilitare solo make_arrayquando i tipi sono uguali, in questo modo:

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

Ad ogni modo, avrai bisogno del all_same<Args...>tratto variadico di tipo. Eccolo, generalizzando std::is_same<S, T>(notare che decadente è importante per consentire la miscelazione di 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; };

Si noti che make_array()restituisce per copia temporanea, che il compilatore (con flag di ottimizzazione sufficienti!) Può trattare come un valore o altrimenti ottimizzare, ed std::arrayè un tipo aggregato, quindi il compilatore è libero di scegliere il miglior metodo di costruzione possibile .

Infine, si noti che non è possibile evitare la costruzione di copia / spostamento quando si make_arrayimposta l'inizializzatore. Quindi std::array<Foo,2> x{Foo(1), Foo(2)};non ha copia / sposta, ma auto x = make_array(Foo(1), Foo(2));ha due copie / mosse quando gli argomenti vengono inoltrati make_array. Non penso che tu possa migliorarlo, perché non puoi passare un elenco di inizializzatori variadici in modo lessicale all'helper e dedurre tipo e dimensioni - se il preprocessore avesse una sizeof...funzione per argomenti variadici, forse potrebbe essere fatto, ma non nel linguaggio principale.


13

L'uso della sintassi di ritorno finale make_arraypuò essere ulteriormente semplificato

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

Sfortunatamente per le classi aggregate richiede una specifica esplicita del tipo

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

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

In effetti questa make_arrayimplementazione è elencata in sizeof ... operatore


versione c ++ 17

Grazie alla deduzione dell'argomento template per la proposta di template di classe possiamo usare le guide alla detrazione per sbarazzarci make_arraydell'helper

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

Compilato con -std=c++1zflag sotto x86-64 gcc 7.0


6
Per questo C ++ 17 dovrebbe già avere una guida alla detrazione: en.cppreference.com/w/cpp/container/array/deduction_guides
underscore_d

6

C ++ 11 supporterà questo modo di inizializzazione per (la maggior parte?) Contenitori standard.


1
Tuttavia, penso che OP non voglia specificare le dimensioni dell'array, ma size è un parametro template di std :: array. Quindi hai bisogno di qualcosa come std :: array <unsigned int, 5> n = {1,2,3,4,5};
juanchopanza,

std::vector<>non ha bisogno dell'intero esplicito e non sono sicuro del perché std::array.
Richard,

@Richard, perché std :: vector ha dimensioni dinamiche e std :: array ha dimensioni fisse. Vedi questo: en.wikipedia.org/wiki/Array_(C%2B%2B)
juanchopanza,

@juanchopanza ma la {...}sintassi implica l'estensione costante del tempo di compilazione, quindi il ctor dovrebbe essere in grado di dedurne l'estensione.
Richard,

1
std::initializer_list::sizenon è una constexprfunzione e quindi non può essere utilizzata in questo modo. Tuttavia, ci sono piani da libstdc ++ (l'implementazione fornita con GCC) per avere la loro versione constexpr.
Luc Danton,

6

So che è passato un po 'di tempo da quando questa domanda è stata posta, ma ritengo che le risposte esistenti presentino ancora alcune carenze, quindi vorrei proporre la mia versione leggermente modificata. Di seguito sono riportati i punti che ritengo che manchino alcune risposte esistenti.


1. Non è necessario fare affidamento su RVO

Alcune risposte affermano che dobbiamo affidarci a RVO per restituire il prodotto array. Quello non è vero; possiamo fare uso dell'inizializzazione dell'elenco copie per garantire che non verranno mai creati dei provvisori. Quindi invece di:

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

dovremmo fare:

return {{values}};

2. Crea make_arrayuna constexprfunzione

Questo ci consente di creare array costanti in fase di compilazione.

3. Non è necessario verificare che tutti gli argomenti siano dello stesso tipo

Prima di tutto, in caso contrario, il compilatore emetterà comunque un avviso o un errore perché l'inizializzazione dell'elenco non consente il restringimento. In secondo luogo, anche se decidessimo davvero di fare le nostre static_assertcose (forse per fornire un messaggio di errore migliore), dovremmo probabilmente confrontare i tipi decaduti degli argomenti piuttosto che i tipi non elaborati. Per esempio,

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

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

Se lo stiamo semplicemente static_assertutilizzando a, be cabbiamo lo stesso tipo, allora questo controllo fallirà, ma probabilmente non è quello che ci aspetteremmo. Invece, dovremmo confrontare i loro std::decay_t<T>tipi (che sono tutti ints)).

4. Ridurre il tipo di valore di array decadendo gli argomenti inoltrati

È simile al punto 3. Utilizzo dello stesso frammento di codice, ma questa volta non specificare esplicitamente il tipo di valore:

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

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

Probabilmente vogliamo crearne uno array<int, 3>, ma le implementazioni nelle risposte esistenti probabilmente non riescono a farlo. Quello che possiamo fare è, invece di restituire a std::array<T, …>, restituire a std::array<std::decay_t<T>, …>.

Questo approccio presenta uno svantaggio: non è più possibile restituire un arraytipo di valore con certificazione cv. Ma il più delle volte, invece di qualcosa di simile a un array<const int, …>, const array<int, …>useremmo comunque uno . C'è un compromesso, ma penso che sia ragionevole. Anche il C ++ 17 std::make_optionaladotta questo approccio:

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

Tenendo conto dei punti precedenti, un'implementazione pienamente funzionante make_arrayin C ++ 14 è simile alla seguente:

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

Uso:

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

5

(Soluzione di @dyp)

Nota: richiede C ++ 14 ( std::index_sequence). Sebbene si possa implementare std::index_sequencein 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;
}

Ho trascurato l'inizializzazione predefinita degli elementi std :: array. Attualmente alla ricerca di una soluzione.
Gabriel Garcia,

@dyp Ho aggiornato la risposta con il tuo codice. Se decidi di scrivere la tua risposta, fammi sapere e io metterò giù la mia. Grazie.
Gabriel Garcia,

1
No, va bene. Associare un array temporaneo per dedurne la lunghezza è una tua idea, e non ho verificato se il mio codice fosse nemmeno compilato. Penso che sia ancora la tua soluzione, e rispondi, con un po 'di raffinatezza;) Si potrebbe obiettare, tuttavia, che non ci sono benefici per un variadico make_arraycome nella risposta di Puppy.
dyp,

Destra. Inoltre, i modelli non possono dedurre i tipi dagli elenchi di inizializzatori, che è uno dei requisiti della domanda (inizializzazione con parentesi nidificate).
Gabriel Garcia,

1

С ++ 17 implementazione compatta.

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


0

Creare un tipo di creatore di array.

Si sovraccarica operator,per generare un modello di espressione che concatena ogni elemento al precedente tramite riferimenti.

Aggiungi una finishfunzione gratuita che prende il creatore di array e genera un array direttamente dalla catena di riferimenti.

La sintassi dovrebbe essere simile a questa:

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

Non consente la {}costruzione basata, come solo operator=. Se sei disposto a usare =possiamo farlo funzionare:

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

o

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

Nessuna di queste sembra una buona soluzione.

L'uso di variardics ti limita al limite imposto dal compilatore sul numero di vararg e blocca l'uso ricorsivo di {}per sottostrutture.

Alla fine, non esiste davvero una buona soluzione.

Quello che faccio è scrivere il mio codice in modo che consumi sia i dati T[]che i std::arraydati in modo agnostico - non importa quale feed. A volte questo significa che il mio codice di inoltro deve trasformare con attenzione le []matrici in modo std::arraytrasparente.


1
"Questi non sembrano buone soluzioni." È anche quello che vorrei dire: p
caps
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.