Übergeben eines std :: -Arrays unbekannter Größe an eine Funktion

96

Wie würde ich in C ++ 11 vorgehen, um eine Funktion (oder Methode) zu schreiben, die ein std :: -Array bekannten Typs, aber unbekannter Größe verwendet?

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

Während meiner Suche habe ich nur Vorschläge zur Verwendung von Vorlagen gefunden, aber diese scheinen chaotisch (Methodendefinitionen im Header) und übertrieben für das, was ich erreichen möchte.

Gibt es eine einfache Möglichkeit, diese Funktion zu verwenden, wie dies bei einfachen Arrays im C-Stil der Fall wäre?

Adrian
quelle
1
Arrays haben keine Grenzen, die prüfen oder wissen, welche Größe sie haben. Daher müssen Sie sie in etwas einwickeln oder die Verwendung von in Betracht ziehen std::vector.
Travis Pessetto
19
Wenn Ihnen Vorlagen unordentlich und übertrieben erscheinen, sollten Sie dieses Gefühl überwinden. Sie sind in C ++ alltäglich.
Benjamin Lindley
std::vectorGibt es einen Grund, nicht wie von @TravisPessetto empfohlen zu verwenden?
Cory Klein
2
Verstanden. Wenn dies eine Einschränkung ihrer Natur ist, muss ich das akzeptieren. Der Grund, warum ich darüber nachgedacht habe, std :: vector zu vermeiden (was für mich sehr gut funktioniert), ist, dass es auf dem Heap zugewiesen ist. Da diese Arrays winzig sind und in jeder Iteration des Programms durchlaufen werden, dachte ich, dass ein std :: -Array eine etwas bessere Leistung erzielen könnte. Ich denke, ich werde dann ein Array im C-Stil verwenden, mein Programm ist nicht komplex.
Adrian
15
@Adrian Deine Art, über Leistung nachzudenken, ist völlig falsch. Versuchen Sie nicht, Mikrooptimierungen vorzunehmen, bevor Sie überhaupt ein funktionierendes Programm haben. Und nachdem Sie ein Programm haben, raten Sie nicht, was optimiert werden soll, sondern lassen Sie sich von einem Profiler sagen, welcher Teil des Programms optimiert werden soll.
Paul Manta

Antworten:

85

Gibt es eine einfache Möglichkeit, diese Funktion zu verwenden, wie dies bei einfachen Arrays im C-Stil der Fall wäre?

Nein , Sie können wirklich nicht tun , wenn Sie Ihre Funktion eine Funktion machen Vorlage (oder eine andere Art von Behälter verwenden, wie ein std::vector, wie es in den Kommentaren zu der Frage vorgeschlagen):

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

Hier ist ein Live-Beispiel .

Andy Prowl
quelle
7
Das OP fragt, ob es eine andere Lösung als Vorlagen gibt.
Novak
1
@Adrian: Leider gibt es keine andere Lösung, wenn Sie möchten, dass Ihre Funktion generisch auf Arrays jeder Größe funktioniert ...
Andy Prowl
1
Richtig: Es gibt keinen anderen Weg. Da jedes std :: array mit einer anderen Größe einen anderen Typ hat, müssen Sie eine Funktion schreiben, die mit verschiedenen Typen arbeiten kann. Vorlagen sind daher die Lösung für std :: array.
Bstamour
4
Das Schöne an der Verwendung einer Vorlage hier ist, dass Sie diese noch allgemeiner gestalten können, sodass sie mit jedem Sequenzcontainer sowie mit Standardarrays funktioniert:template<typename C, typename M> void mulArray(C & arr, M multiplier) { /* same body */ }
Benjamin Lindley
1
@BenjaminLindley: Das setzt natürlich voraus, dass er den Code überhaupt in den Header einfügen kann.
Nicol Bolas
25

Die Größe von arrayist Teil des Typs , sodass Sie nicht ganz das tun können, was Sie wollen. Es gibt ein paar Alternativen.

Bevorzugt wäre es, ein Paar Iteratoren zu nehmen:

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

Verwenden Sie alternativ vectoranstelle des Arrays, damit Sie die Größe zur Laufzeit und nicht als Teil des Typs speichern können:

void mulArray(std::vector<int>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}
Mark B.
quelle
1
Ich denke, das ist die überlegene Lösung. Wenn Sie sich die Mühe machen, eine Vorlage zu erstellen, machen Sie sie mit Iteratoren vollständig generisch, sodass Sie jeden Container (Array, Liste, Vektor, sogar C-Zeiger der alten Schule usw.) ohne Nachteile verwenden können. Danke für den Tipp.
Mark Lakata
5

Ich habe es unten versucht und es hat nur bei mir funktioniert.

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

AUSGABE :

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

Moschus
quelle
2
Dies ist kein gültiges C ++, sondern eine Erweiterung. Diese Funktionen sind Vorlagen, auch ohne template.
HolyBlackCat
Ich habe dies untersucht und es scheint, dass auto foo(auto bar) { return bar * 2; }C ++ derzeit nicht gültig ist, obwohl es in GCC7 mit gesetztem C ++ 17-Flag kompiliert wird. Nach dem Lesen hier sind als auto deklarierte Funktionsparameter Teil des Concepts TS, der eventuell Teil von C ++ 20 sein sollte.
Fibbles
Warnung C26485
Metablaster
4

BEARBEITEN

C ++ 20 enthält vorläufig std::span

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

Ursprüngliche Antwort

Was Sie wollen, ist so etwas wie das gsl::span, was in der Guideline Support Library verfügbar ist , die in den C ++ Core Guidelines beschrieben ist:

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

Eine Open-Source-Implementierung der GSL nur für Header finden Sie hier:

https://github.com/Microsoft/GSL

Mit gsl::spankönnen Sie dies tun:

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

Das Problem dabei std::arrayist, dass seine Größe Teil seines Typs ist, sodass Sie eine Vorlage verwenden müssen, um eine Funktion zu implementieren, die eine std::arraybeliebige Größe hat.

gsl::spanspeichert andererseits seine Größe als Laufzeitinformation. Auf diese Weise können Sie eine Nicht-Vorlagenfunktion verwenden, um ein Array beliebiger Größe zu akzeptieren. Es werden auch andere zusammenhängende Container akzeptiert:

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

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

Ziemlich cool, oder?

Suncho
quelle
3

Absolut, es gibt in C ++ 11 eine einfache Möglichkeit, eine Funktion zu schreiben, die ein std :: -Array bekannten Typs, aber unbekannter Größe verwendet.

Wenn wir die Arraygröße nicht an die Funktion übergeben können, können wir stattdessen die Speicheradresse des Startpunkts des Arrays zusammen mit einer zweiten Adresse des Endpunkts des Arrays übergeben. Später können wir innerhalb der Funktion diese 2 Speicheradressen verwenden, um die Größe des Arrays zu berechnen!

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

Ausgabe an der Konsole: 10, 20, 2, 4, 8

David M. Helmuth
quelle
1

Dies ist möglich, erfordert jedoch einige Schritte, um es sauber zu machen. Schreiben Sie zunächst a template class, das einen Bereich zusammenhängender Werte darstellt. Leiten Sie dann eine templateVersion, die weiß, wie groß die arrayist, an die ImplVersion weiter, die diesen zusammenhängenden Bereich einnimmt.

Implementieren Sie abschließend die contig_rangeVersion. Beachten Sie, dass dies for( int& x: range )funktioniert contig_range, da ich implementiert habe begin()und end()und Zeiger Iteratoren sind.

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

(nicht getestet, aber das Design sollte funktionieren).

Dann in Ihrer .cppDatei:

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

Dies hat den Nachteil, dass der Code, der den Inhalt des Arrays durchläuft, (zur Kompilierungszeit) nicht weiß, wie groß das Array ist, was zu einer Kostenoptimierung führen kann. Dies hat den Vorteil, dass sich die Implementierung nicht im Header befinden muss.

Seien Sie vorsichtig beim expliziten Erstellen von a contig_range, als ob Sie es übergeben würden. setEs wird davon ausgegangen, dass die setDaten zusammenhängend sind, was falsch ist, und überall undefiniertes Verhalten zeigen. Die einzigen zwei stdContainer, an denen dies garantiert funktioniert, sind vectorund array(und Arrays im C-Stil, wie es passiert!). dequeObwohl der Direktzugriff nicht zusammenhängend ist (gefährlich, er ist in kleinen Stücken zusammenhängend!), listist er nicht einmal in der Nähe und die assoziativen (geordneten und ungeordneten) Container sind gleichermaßen nicht zusammenhängend.

Also die drei Konstruktoren, die ich wo implementiert habe std::array, std::vectorund C-artige Arrays, die im Grunde die Basen abdecken.

Die Implementierung []ist ebenfalls einfach, und dazwischen for()und []das ist das meiste, wofür Sie sich wünschen array, nicht wahr?

Yakk - Adam Nevraumont
quelle
Versetzt dies nicht nur die Vorlage an einen anderen Ort?
GManNickG
@ GManNickG Art von. Der Header erhält eine sehr kurze templateFunktion ohne Implementierungsdetails. Die ImplFunktion ist keine templateFunktion, sodass Sie die Implementierung problemlos in der .cppDatei Ihrer Wahl ausblenden können . Es ist eine wirklich grobe Art der Typlöschung, bei der ich die Fähigkeit extrahiere, über zusammenhängende Container in eine einfachere Klasse zu iterieren und diese dann durchzuleiten ... (während multArrayImpla templateals Argument genommen wird, ist es kein templateSelbst).
Yakk - Adam Nevraumont
Ich verstehe, dass diese Array-Ansicht / Array-Proxy-Klasse manchmal nützlich ist. Mein Vorschlag wäre, den Anfang / das Ende des Containers im Konstruktor zu übergeben, damit Sie nicht für jeden Container einen Konstruktor schreiben müssen. Außerdem würde ich '& * std :: begin (arr)' nicht schreiben, da eine Dereferenzierung und das Aufnehmen der Adresse hier nicht erforderlich ist, da std :: begin / std :: end bereits einen Iterator zurückgibt.
Ricky65
@ Ricky65 Wenn Sie Iteratoren verwenden, müssen Sie die Implementierung verfügbar machen. Wenn Sie Zeiger verwenden, tun Sie dies nicht. Die &*Dereferenzierung des Iterators (der möglicherweise kein Zeiger ist) macht dann einen Zeiger auf die Adresse. Bei zusammenhängenden Speicherdaten sind der Zeiger auf beginund der Zeiger auf one-past-the endebenfalls Iteratoren mit wahlfreiem Zugriff, und sie sind für jeden zusammenhängenden Bereich über einen Typ vom gleichen Typ T.
Yakk - Adam Nevraumont