Comment émuler l'initialisation du tableau C "int arrr[] = {e1, e2, e3, ...}" comportement avec std::array?

(Note: cette question concerne le fait de ne pas avoir à spécifier le nombre d'éléments et de permettre tout de même aux types imbriqués d'être directement initialisés.)

cette question discute les utilisations laissées pour un tableau C comme int arr[20]; . Sur sa réponse , @James Kanze montre l'un des derniers bastions des tableaux C, c'est unique caractéristiques d'initialisation:

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

nous pas besoin de préciser le nombre d'éléments, hourra! Maintenant itérez-le avec les fonctions C++11 std::begin et std::end de <iterator> ( ou vos propres variantes ) et vous n'avez même pas besoin de penser à sa taille.

maintenant, y a-t-il des moyens (éventuellement TMP) pour obtenir la même chose avec std::array ? Utilisation de macros permis de le faire paraître plus agréable. :)

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

modifier : version intermédiaire, compilée à partir de diverses réponses, ressemble à ceci:

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

et emploie toute sorte de cool C++11 stuff:

  • Variadic Templates
  • sizeof...
  • références rvalue
  • perfect forwarding
  • std::array , bien sûr
  • initialisation uniforme
  • omettant le type de retour avec une initialisation uniforme
  • type inférence ( auto )

Et un exemple peut être trouvé ici .

cependant , comme @Johannes le souligne dans le commentaire sur la réponse de @Xaade, vous ne pouvez pas initialiser les types imbriqués avec une telle fonction. Exemple:

struct A{ int a; int b; };

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

aussi, le nombre d'initialiseurs est limité au nombre d'arguments de fonction et de modèle pris en charge par la mise en œuvre.

123
demandé sur Community 2011-05-24 21:00:13

9 réponses

le mieux que je puisse penser est:

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

cependant, cela nécessite que le compilateur fasse NRVO, et puis sauter également la copie de la valeur retournée (qui est également légale mais pas nécessaire). En pratique, je m'attendrais à ce que n'importe quel compilateur C++ soit capable de l'optimiser de telle sorte qu'il soit aussi rapide que l'initialisation directe.

56
répondu Pavel Minaev 2011-05-24 17:40:42

Je m'attendais à un simple 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)... } };
}
36
répondu Puppy 2015-01-15 09:14:31

combinant quelques idées des billets précédents, voici une solution qui fonctionne même pour les constructions emboîtées (testé dans 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)...};
}

étrangement, ne peut pas faire la valeur de retour une référence de valeur, qui ne fonctionnerait pas pour les constructions imbriquées. Bref, voici 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]]]

(pour la dernière sortie j'utilise mon pretty-printer .)


En fait, améliorons la sécurité de type de cette construction. Nous avons vraiment besoin que tous les types soient les mêmes. Une façon est d'ajouter une assertion statique, que j'ai éditée ci-dessus. L'autre façon est d'activer make_array quand les types sont les mêmes, comme suit:

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

dans tous les cas, vous aurez besoin du trait de type variadique all_same<Args...> . La voici, généralisant de std::is_same<S, T> (notez que la décomposition est importante pour permettre le mélange de 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; };

notez que make_array() retourne par copie-de-temporaire, que le compilateur (avec des indicateurs d'optimisation suffisants! std::array est un agrégat, de sorte que le compilateur est libre de choisir la meilleure méthode de construction possible.

enfin, notez que vous ne pouvez pas éviter la construction copie / déplacement lorsque make_array met en place l'initialiseur. So std::array<Foo,2> x{Foo(1), Foo(2)}; n'a pas de copie/mouvement, mais auto x = make_array(Foo(1), Foo(2)); a deux copies/mouvements car les arguments sont transmis à make_array . Je ne pense pas que vous pouvez améliorer sur cela, parce que vous ne pouvez pas passer une liste d'initialiseur variadique lexiquement à l'Assistant et déduire le type et la taille-si le préprocesseur avait une fonction sizeof... pour les arguments variadique, peut-être que cela pourrait être fait, mais pas dans le langage de base.

20
répondu Kerrek SB 2017-05-23 12:09:50

en utilisant la syntaxe de retour arrière make_array peut être encore simplifié

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

malheureusement pour les classes agrégées, il faut une spécification de type explicite

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

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

en fait cette make_array mise en œuvre est énumérée dans sizeof... exploitant


C++17 version

merci à déduction d'argument de modèle pour les modèles de classe proposition nous pouvons utiliser des guides de déduction pour se débarrasser de make_array 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; 
}

compilé avec -std=c++1z drapeau sous x86-64 gcc 7.0

9
répondu wiped 2017-04-13 09:29:20

C++11 supportera cette manière d'initialisation pour (most?) std conteneurs.

6
répondu Richard 2018-04-26 14:51:56

(Solution de @dyp)

Note: nécessite C++14 ( std::index_sequence ). Bien qu'on puisse implémenter std::index_sequence en 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;
}
5
répondu Gabriel Garcia 2014-12-30 18:04:55

je sais que cela fait un certain temps que cette question a été posée, mais je pense que les réponses existantes ont encore quelques lacunes, donc je voudrais proposer ma version légèrement modifiée. Suivantes sont les points que je pense que certaines réponses sont manquantes.


1. Pas besoin de compter sur RVO

certaines réponses mentionnent que nous devons compter sur RVO pour retourner le array construit . Ce n'est pas vrai; nous pouvons faire utilisation de copy-list-initialization pour garantir qu'il n'y aura jamais de temporaires créés. Donc au lieu de:

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

nous devrions faire:

return {{values}};

2. Faire make_array a constexpr fonction

cela nous permet de créer des tableaux de constantes de temps de compilation.

3. Pas besoin de vérifier que tous les arguments sont du même type

D'abord, s'ils ne le sont pas, le compilateur émettra un avertissement ou une erreur de toute façon parce que l'initialisation par liste ne permet pas de rétrécir. Deuxièmement, même si nous décidons vraiment de faire notre propre static_assert chose (peut-être pour fournir un meilleur message d'erreur), nous devrions encore probablement comparer les arguments types décomposés plutôt que les types bruts. Par exemple,

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

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

si nous sommes simplement static_assert ing que a , b , et c ont le même type, alors cette vérification échouera, mais ce n'est probablement pas ce à quoi on s'attend. Au lieu de cela, nous devrions comparer leurs types std::decay_t<T> (qui sont tous int s)).

4. Déduisez le type de valeur du tableau en décomposant les arguments transmis

similaire au point 3. En utilisant le même snippet de code, mais ne spécifiez pas le type de valeur explicitement cette fois:

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

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

nous voulons probablement faire un array<int, 3> , mais les implémentations dans les réponses existantes ne le font probablement pas toutes. Ce que nous pouvons faire, c'est , au lieu de retourner un std::array<T, …> , retourner un std::array<std::decay_t<T>, …> .

il y a un inconvénient à cette approche: nous ne pouvons plus retourner un array de type cv-qualified value. Mais la plupart du temps , au lieu de quelque chose comme un array<const int, …> , nous utilisions un const array<int, …> de toute façon. Il y a un compromis, mais je pense qu'il est raisonnable. Le C++17 std::make_optional prend également cette approche:

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

compte tenu de ce qui précède, une mise en œuvre complète de make_array en C++14 ressemble à ceci:

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

Utilisation:

constexpr auto arr = make_array(make_array(1, 2),
                                make_array(3, 4));
static_assert(arr[1][1] == 4, "!");
4
répondu Zizheng Tai 2016-07-11 03:04:25

si std:: array n'est pas une contrainte et si vous avez Boost, alors jetez un oeil à list_of() . Ce n'est pas exactement comme l'initialisation de tableau de type C que vous voulez. Mais fermer.

0
répondu yasouser 2011-05-24 17:14:34

créer un type de fabricant de tableau.

il télécharge operator, pour générer un modèle d'expression enchaînant chaque élément aux références via précédentes.

ajouter une finish fonction libre qui prend le fabricant de tableau et génère un tableau directement à partir de la chaîne de références.

la syntaxe doit ressembler à quelque chose comme ceci:

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

il ne permet pas {} basé construction, comme seul operator= le fait. Si vous êtes prêt à utiliser = nous pouvons le faire fonctionner:

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

ou

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

aucune de ces solutions ne semble bonne.

L'utilisation de variardics vous limite à la limite imposée par le compilateur sur le nombre de varargs et bloque l'utilisation récursive de {} pour les sous-structures.

finalement, il n'y a vraiment pas de bonne solution.

ce que je fais c'est écrire mon code pour qu'il consume à la fois T[] et std::array données agnostiquement -- il ne se soucie pas de ce que je l'alimente. Parfois, cela signifie que mon code d'expédition doit soigneusement transformer les matrices [] en std::array de manière transparente.

0
répondu Yakk - Adam Nevraumont 2016-08-25 20:23:15