Passer un std :: array de taille inconnue à une fonction

98

En C ++ 11, comment pourrais-je écrire une fonction (ou une méthode) qui prend un std :: array de type connu mais de taille inconnue?

// made up example
void mulArray(std::array<int, ?>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}

// lets imagine these being full of numbers
std::array<int, 17> arr1;
std::array<int, 6>  arr2;
std::array<int, 95> arr3;

mulArray(arr1, 3);
mulArray(arr2, 5);
mulArray(arr3, 2);

Au cours de ma recherche, je n'ai trouvé que des suggestions d'utilisation de modèles, mais celles-ci semblent désordonnées (définitions de méthode dans l'en-tête) et excessives pour ce que j'essaie d'accomplir.

Existe-t-il un moyen simple de faire fonctionner cela, comme on le ferait avec des tableaux simples de style C?

Adrian
la source
1
Les tableaux n'ont pas de limites pour vérifier ou savoir de quelle taille ils sont. Par conséquent, vous devez les envelopper dans quelque chose ou envisager d'utiliser std::vector.
Travis Pessetto
20
Si les modèles vous semblent désordonnés et excessifs, vous devriez surmonter ce sentiment. Ils sont courants en C ++.
Benjamin Lindley
Une raison de ne pas utiliser std::vectorcomme @TravisPessetto le recommande?
Cory Klein
2
Compris. Si c'est une limitation de leur nature, je devrai l'accepter. La raison pour laquelle j'ai pensé à éviter std :: vector (qui fonctionne très bien pour moi), c'est qu'il est alloué sur le tas. Étant donné que ces tableaux seront minuscules et bouclés à chaque itération du programme, j'ai pensé qu'un std :: array pourrait fonctionner un peu mieux. Je pense que je vais utiliser un tableau de style C alors, mon programme n'est pas complexe.
Adrian
15
@Adrian Votre façon de penser la performance est totalement erronée. N'essayez pas de faire des micro-optimisations avant même d'avoir un programme fonctionnel. Et une fois que vous avez un programme, ne devinez pas ce qui doit être optimisé, laissez plutôt un profileur vous dire quelle partie du programme doit être optimisée.
Paul Manta

Réponses:

86

Existe-t-il un moyen simple de faire fonctionner cela, comme on le ferait avec des tableaux simples de style C?

Non. Vous ne pouvez vraiment pas faire cela à moins de faire de votre fonction un modèle de fonction (ou d'utiliser un autre type de conteneur, comme un std::vector, comme suggéré dans les commentaires de la question):

template<std::size_t SIZE>
void mulArray(std::array<int, SIZE>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}

Voici un exemple en direct .

Andy Prowl
la source
8
L'OP demande s'il existe une autre solution que des modèles.
Novak
1
@Adrian: Malheureusement, il n'y a pas d'autre solution, si vous voulez que votre fonction fonctionne de manière générique sur des tableaux de toute taille ...
Andy Prowl
1
Correct: il n'y a pas d'autre moyen. Comme chaque std :: array avec une taille différente est un type différent, vous devez écrire une fonction qui peut fonctionner sur différents types. Par conséquent, les modèles sont la solution pour std :: array.
bstamour
4
La belle partie de l'utilisation d'un modèle ici est que vous pouvez le rendre encore plus générique, afin qu'il fonctionne avec n'importe quel conteneur de séquence, ainsi que des tableaux standard:template<typename C, typename M> void mulArray(C & arr, M multiplier) { /* same body */ }
Benjamin Lindley
1
@BenjaminLindley: Bien sûr, cela suppose qu'il peut mettre le code dans l'en-tête du tout.
Nicol Bolas
26

La taille du arrayfait partie du type , vous ne pouvez donc pas faire tout ce que vous voulez. Il existe plusieurs alternatives.

Il serait préférable de prendre une paire d'itérateurs:

template <typename Iter>
void mulArray(Iter first, Iter last, const int multiplier) {
    for(; first != last; ++first) {
        *first *= multiplier;
    }
}

Sinon, utilisez vectorau lieu de array, qui vous permet de stocker la taille au moment de l'exécution plutôt que dans le cadre de son type:

void mulArray(std::vector<int>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}
Marque B
la source
1
Je pense que c'est la meilleure solution; si vous allez avoir la peine de créer un modèle, rendez-le totalement générique avec des itérateurs qui vous permettront d'utiliser n'importe quel conteneur (tableau, liste, vecteur, même des pointeurs C de la vieille école, etc.) sans inconvénient. Merci pour l'indice.
Mark Lakata le
6

J'ai essayé ci-dessous et cela a fonctionné pour moi.

#include <iostream>
#include <array>

using namespace std;

// made up example
void mulArray(auto &arr, const int multiplier) 
{
    for(auto& e : arr) 
    {
        e *= multiplier;
    }
}

void dispArray(auto &arr)
{
    for(auto& e : arr) 
    {
        std::cout << e << " ";
    }
    std::cout << endl;
}

int main()
{

    // lets imagine these being full of numbers
    std::array<int, 7> arr1 = {1, 2, 3, 4, 5, 6, 7};
    std::array<int, 6> arr2 = {2, 4, 6, 8, 10, 12};
    std::array<int, 9> arr3 = {1, 1, 1, 1, 1, 1, 1, 1, 1};

    dispArray(arr1);
    dispArray(arr2);
    dispArray(arr3);

    mulArray(arr1, 3);
    mulArray(arr2, 5);
    mulArray(arr3, 2);

    dispArray(arr1);
    dispArray(arr2);
    dispArray(arr3);

    return 0;
}

PRODUCTION :

1 2 3 4 5 6 7

2 4 6 8 10 12

1 1 1 1 1 1 1 1 1

3 6 9 12 15 18 21

10 20 30 40 50 60

2 2 2 2 2 2 2 2 2

musc
la source
3
Ce n'est pas du C ++ valide, mais plutôt une extension. Ces fonctions sont des modèles, même sans template.
HolyBlackCat
1
J'ai regardé cela et il semble que ce auto foo(auto bar) { return bar * 2; }n'est pas actuellement C ++ valide même s'il compile dans GCC7 avec le jeu d'indicateurs C ++ 17. D'après la lecture ici , les paramètres de fonction déclarés comme auto font partie des concepts TS qui devraient éventuellement faire partie de C ++ 20.
Fibbles
Avertissement C26485
metablaster
5

ÉDITER

C ++ 20 inclut provisoirement std::span

https://en.cppreference.com/w/cpp/container/span

Réponse originale

Ce que vous voulez, c'est quelque chose comme gsl::span, qui est disponible dans la bibliothèque de prise en charge des lignes directrices décrite dans les lignes directrices de base C ++:

https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md#SS-views

Vous pouvez trouver une implémentation open-source de la GSL uniquement en-tête ici:

https://github.com/Microsoft/GSL

Avec gsl::span, vous pouvez faire ceci:

// made up example
void mulArray(gsl::span<int>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}

// lets imagine these being full of numbers
std::array<int, 17> arr1;
std::array<int, 6>  arr2;
std::array<int, 95> arr3;

mulArray(arr1, 3);
mulArray(arr2, 5);
mulArray(arr3, 2);

Le problème avec std::arrayest que sa taille fait partie de son type, vous devez donc utiliser un modèle pour implémenter une fonction qui prend une std::arraytaille arbitraire.

gsl::spand'autre part, stocke sa taille en tant qu'informations d'exécution. Cela vous permet d'utiliser une fonction non-modèle pour accepter un tableau de taille arbitraire. Il acceptera également d'autres conteneurs contigus:

std::vector<int> vec = {1, 2, 3, 4};
int carr[] = {5, 6, 7, 8};

mulArray(vec, 6);
mulArray(carr, 7);

Assez cool, hein?

suncho
la source
3

Absolument, il existe un moyen simple en C ++ 11 d'écrire une fonction qui prend un std :: array de type connu, mais de taille inconnue.

Si nous ne pouvons pas transmettre la taille du tableau à la fonction, alors à la place, nous pouvons transmettre l'adresse mémoire de l'endroit où le tableau commence avec une deuxième adresse de l'endroit où le tableau se termine. Plus tard, à l'intérieur de la fonction, nous pouvons utiliser ces 2 adresses mémoire pour calculer la taille du tableau!

#include <iostream>
#include <array>

// The function that can take a std::array of any size!
void mulArray(int* piStart, int* piLast, int multiplier){

     // Calculate the size of the array (how many values it holds)
     unsigned int uiArraySize = piLast - piStart;

     // print each value held in the array
     for (unsigned int uiCount = 0; uiCount < uiArraySize; uiCount++)     
          std::cout << *(piStart + uiCount) * multiplier << std::endl;
}

int main(){   

     // initialize an array that can can hold 5 values
     std::array<int, 5> iValues;

     iValues[0] = 5;
     iValues[1] = 10;
     iValues[2] = 1;
     iValues[3] = 2;
     iValues[4] = 4;

     // Provide a pointer to both the beginning and end addresses of 
     // the array.
     mulArray(iValues.begin(), iValues.end(), 2);

     return 0;
}

Sortie à la console: 10, 20, 2, 4, 8

David M. Helmuth
la source
1

Cela peut être fait, mais il faut quelques étapes pour le faire proprement. Tout d'abord, écrivez un template classqui représente une plage de valeurs contiguës. Ensuite, transférez une templateversion qui connaît la taille arrayde la Implversion qui prend cette plage contiguë.

Enfin, implémentez la contig_rangeversion. Notez que cela for( int& x: range )fonctionne pour contig_range, car j'ai implémenté begin()et end()et les pointeurs sont des itérateurs.

template<typename T>
struct contig_range {
  T* _begin, _end;
  contig_range( T* b, T* e ):_begin(b), _end(e) {}
  T const* begin() const { return _begin; }
  T const* end() const { return _end; }
  T* begin() { return _begin; }
  T* end() { return _end; }
  contig_range( contig_range const& ) = default;
  contig_range( contig_range && ) = default;
  contig_range():_begin(nullptr), _end(nullptr) {}

  // maybe block `operator=`?  contig_range follows reference semantics
  // and there really isn't a run time safe `operator=` for reference semantics on
  // a range when the RHS is of unknown width...
  // I guess I could make it follow pointer semantics and rebase?  Dunno
  // this being tricky, I am tempted to =delete operator=

  template<typename T, std::size_t N>
  contig_range( std::array<T, N>& arr ): _begin(&*std::begin(arr)), _end(&*std::end(arr)) {}
  template<typename T, std::size_t N>
  contig_range( T(&arr)[N] ): _begin(&*std::begin(arr)), _end(&*std::end(arr)) {}
  template<typename T, typename A>
  contig_range( std::vector<T, A>& arr ): _begin(&*std::begin(arr)), _end(&*std::end(arr)) {}
};

void mulArrayImpl( contig_range<int> arr, const int multiplier );

template<std::size_t N>
void mulArray( std::array<int, N>& arr, const int multiplier ) {
  mulArrayImpl( contig_range<int>(arr), multiplier );
}

(non testé, mais la conception devrait fonctionner).

Ensuite, dans votre .cppdossier:

void mulArrayImpl(contig_range<int> rng, const int multiplier) {
  for(auto& e : rng) {
    e *= multiplier;
  }
}

Cela a l'inconvénient que le code qui boucle sur le contenu du tableau ne sait pas (au moment de la compilation) la taille du tableau, ce qui pourrait coûter l'optimisation. Il présente l'avantage que l'implémentation ne doit pas nécessairement figurer dans l'en-tête.

Faites attention à ne pas construire explicitement a contig_range, comme si vous le transmettez a setcela supposera que les setdonnées sont contiguës, ce qui est faux, et fera un comportement indéfini partout. Les deux seuls stdconteneurs sur lesquels cela est garanti de fonctionner sont vectoret array(et les tableaux de style C, en l'occurrence!). dequebien que l'accès aléatoire ne soit pas contigu (dangereusement, il est contigu par petits morceaux!), listn'est même pas proche, et les conteneurs associatifs (ordonnés et non ordonnés) sont également non contigus.

Donc, les trois constructeurs que j'ai implémentés où std::array, std::vectoret les tableaux de style C, qui couvrent essentiellement les bases.

La mise en œuvre []est également facile, et entre for()et []c'est ce que vous recherchez array, n'est-ce pas?

Yakk - Adam Nevraumont
la source
N'est-ce pas simplement compenser le modèle ailleurs?
GManNickG
@GManNickG en quelque sorte. L'en-tête obtient une templatefonction très courte avec pratiquement aucun détail d'implémentation. La Implfonction n'est pas une templatefonction, vous pouvez donc cacher l'implémentation dans le .cppfichier de votre choix. C'est une sorte d'effacement de type vraiment grossier, où j'extrait la capacité d'itérer sur des conteneurs contigus dans une classe plus simple, puis de la passer à travers ... (bien que multArrayImplprenne a templatecomme argument, ce n'est pas un templatelui - même).
Yakk - Adam Nevraumont
Je comprends que cette classe de proxy vue / tableau est parfois utile. Ma suggestion serait de passer le début / la fin du conteneur dans le constructeur afin que vous n'ayez pas à écrire un constructeur pour chaque conteneur. De plus, je n'écrirais pas '& * std :: begin (arr)' car le déréférencement et la prise de l'adresse n'est pas nécessaire ici car std :: begin / std :: end renvoie déjà un itérateur.
Ricky65
@ Ricky65 Si vous utilisez des itérateurs, vous devez exposer l'implémentation. Si vous utilisez des pointeurs, ce n'est pas le cas. Le &*déréférence l'itérateur (qui peut ne pas être un pointeur), puis fait un pointeur vers l'adresse. Pour les données de mémoire contiguës, le pointeur vers beginet le pointeur vers un après-le endsont également des itérateurs à accès aléatoire, et ils sont du même type pour chaque plage contiguë sur un type T.
Yakk - Adam Nevraumont