web-dev-qa-db-fra.com

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

(Remarque: cette question concerne le fait de ne pas avoir à spécifier le nombre d'éléments et à autoriser l'initialisation directe des types imbriqués.)
Cette question discute des utilisations restantes pour un tableau C comme int arr[20];. Sur sa réponse , @James Kanze montre l'un des derniers bastions des tableaux C, ses caractéristiques d'initialisation uniques:

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

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

Maintenant, existe-t-il des moyens (éventuellement TMP) de réaliser la même chose avec std::array? L'utilisation de macros a permis de le rendre plus joli. :)

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

Edit : La 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 utilise toutes sortes de trucs cool C++ 11:

  • Modèles Variadic
  • sizeof...
  • références rvalue
  • expédition parfaite
  • std::array, bien sûr
  • initialisation uniforme
  • en omettant le type de retour avec une initialisation uniforme
  • inférence de type (auto)

Et un exemple peut être trouvé ici .

Cependant , comme le souligne @Johannes 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} };

De plus, le nombre d'initialiseurs est limité au nombre d'arguments de fonction et de modèle pris en charge par l'implémentation.

132
Xeo

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, puis ignore également la copie de la valeur retournée (qui est également légale mais non requise). En pratique, je m'attendrais à ce que n'importe quel compilateur C++ puisse optimiser cela de telle sorte qu'il soit aussi rapide qu'une initialisation directe.

60
Pavel Minaev

Je m'attendrais à 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)... } };
    return { std::forward<T>(refs)... };
}
37
Puppy

En combinant quelques idées des articles précédents, voici une solution qui fonctionne même pour les constructions imbriquées (testée 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 de la valeur de retour une référence rvalue, ce 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 certainement besoin que tous les types soient identiques. Une façon consiste à ajouter une assertion statique, que j'ai modifiée ci-dessus. L'autre façon est de n'activer make_array Que lorsque les types sont les mêmes, comme ceci:

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 à partir 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 suffisamment de drapeaux d'optimisation!) Est autorisé à traiter comme une valeur r ou à optimiser autrement, et std::array Est un type d'agrégat , le compilateur est donc libre de choisir la meilleure méthode de construction possible.

Enfin, notez que vous ne pouvez pas éviter la construction copier/déplacer lorsque make_array Configure l'initialiseur. Ainsi, std::array<Foo,2> x{Foo(1), Foo(2)}; n'a pas de copie/déplacement, mais auto x = make_array(Foo(1), Foo(2)); a deux copie/mouvements lorsque les arguments sont transmis à make_array. Je ne pense pas que vous puissiez améliorer cela, car vous ne pouvez pas transmettre lexicalement une liste d'initialiseurs variadiques à l'aide et en déduire le type et la taille - si le préprocesseur avait une fonction sizeof... pour les arguments variadiques, cela pourrait peut-être être fait, mais pas dans le langage principal.

20
Kerrek SB

Utilisation de la syntaxe de retour de fin make_array peut encore être 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 nécessite 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, ce make_array l'implémentation est répertoriée dans opérateur sizeof ...


version c ++ 17

Grâce à 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 assistant

#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

11
wiped

Je sais que cela fait un bon bout de temps depuis que cette question a été posée, mais je pense que les réponses existantes ont encore des lacunes, alors je voudrais proposer ma version légèrement modifiée. Voici les points auxquels je pense que certaines réponses existantes manquent.


1. Pas besoin de compter sur RVO

Certaines réponses mentionnent que nous devons compter sur RVO pour renvoyer le array construit. Ce n'est pas vrai; nous pouvons utiliser copy-list-initialization pour garantir qu'il n'y aura jamais de création temporaire. Donc au lieu de:

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

nous devrions faire:

return {{values}};

2. Faites make_array une fonction constexpr

Cela nous permet de créer des tableaux de constantes au moment de la compilation.

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

Tout d'abord, s'ils ne le sont pas, le compilateur émettra quand même un avertissement ou une erreur car l'initialisation de la liste ne permet pas de restreindre. 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 probablement comparer les types d'arguments pourris 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_assertsi que a, b et c ont le même type, cette vérification échouera, mais ce n'est probablement pas ce à quoi nous nous attendions. Au lieu de cela, nous devrions comparer leur std::decay_t<T> types (qui sont tous ints)).

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

Ceci est similaire au point 3. En utilisant le même extrait de code, mais ne spécifiez pas explicitement le type de valeur 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 des réponses existantes échouent probablement toutes. Ce que nous pouvons faire, au lieu de renvoyer un std::array<T, …>, retourne un std::array<std::decay_t<T>, …>.

Il y a un inconvénient à cette approche: nous ne pouvons plus renvoyer un array de type valeur qualifié par cv. Mais la plupart du temps, au lieu de quelque chose comme un array<const int, …>, nous utiliserions un const array<int, …> en tous cas. Il y a un compromis, mais je pense qu'il est raisonnable. Le C++ 17 std::make_optional adopte également cette approche:

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

Compte tenu des points ci-dessus, une mise en œuvre fonctionnelle 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 {};
}

Usage:

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

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

6
Richard

(Solution de @dyp)

Remarque: nécessite C++ 14 (std::index_sequence). Bien que l'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
Gabriel Garcia

Si std :: array n'est pas une contrainte et si vous avez Boost, jetez un œil à list_of() . Ce n'est pas exactement comme l'initialisation de tableau de type C que vous souhaitez. Mais proche.

0
yasouser

Créez un type de générateur de baies.

Il surcharge operator, pour générer un modèle d'expression enchaînant chaque élément au précédent via des références.

Ajoutez une fonction gratuite finish qui prend le générateur de tableaux et génère un tableau directement à partir de la chaîne de références.

La syntaxe devrait ressembler à ceci:

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

Il ne permet pas {} construction basée, comme seulement operator= Est-ce que. Si vous souhaitez 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}] );

Aucun de ceux-ci ne ressemble à de bonnes solutions.

L'utilisation de variardics vous limite à votre limite imposée par le compilateur sur le nombre de varargs et de blocs récursifs utilisant {} pour les sous-structures.

En fin de compte, il n'y a vraiment pas de bonne solution.

Ce que je fais, c'est que j'écris mon code pour qu'il consomme à la fois T[] et std::array data agnostiquement - peu importe ce que je lui donne. Parfois, cela signifie que mon code de transfert doit tourner soigneusement [] tableaux en std::arrays de manière transparente.

0