Przekazywanie tablicy std :: o nieznanym rozmiarze do funkcji

102

W C ++ 11, jak powinienem napisać funkcję (lub metodę), która pobiera std :: tablicę znanego typu, ale nieznanego rozmiaru?

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

Podczas moich poszukiwań znalazłem tylko sugestie dotyczące używania szablonów, ale wydają się one niechlujne (definicje metod w nagłówku) i nadmierne w stosunku do tego, co próbuję osiągnąć.

Czy istnieje prosty sposób, aby to zadziałało, tak jak w przypadku zwykłych tablic w stylu C?

Adrian
źródło
1
Tablice nie mają ograniczeń co do sprawdzania lub znajomości ich rozmiaru. Dlatego musisz je czymś owinąć lub rozważyć użycie std::vector.
Travis Pessetto
20
Jeśli szablony wydają Ci się niechlujne i przesadne, powinieneś przeboleć to uczucie. Są powszechne w C ++.
Benjamin Lindley
Czy jest jakiś powód, aby nie używać std::vectorzgodnie z zaleceniami @TravisPessetto?
Cory Klein
2
Zrozumiany. Jeśli jest to ograniczenie ich natury, będę musiał to zaakceptować. Powodem, dla którego myślałem o unikaniu std :: vector (który działa świetnie dla mnie), jest to, że jest on przydzielany na stercie. Ponieważ te tablice będą małe i zapętlone w każdej iteracji programu, pomyślałem, że std :: array może działać nieco lepiej. Myślę, że wtedy użyję tablicy w stylu C, mój program nie jest złożony.
Adrian,
15
@Adrian Twój sposób myślenia o wydajności jest całkowicie błędny. Nie próbuj dokonywać mikrooptymalizacji, zanim jeszcze nie masz funkcjonalnego programu. A po utworzeniu programu nie zgaduj, co powinno zostać zoptymalizowane, zamiast tego pozwól profilerowi powiedzieć, która część programu powinna zostać zoptymalizowana.
Paul Manta

Odpowiedzi:

90

Czy istnieje prosty sposób, aby to zadziałało, tak jak w przypadku zwykłych tablic w stylu C?

Nie. Naprawdę nie możesz tego zrobić, chyba że uczynisz swoją funkcję szablonem funkcji (lub użyjesz innego rodzaju kontenera, np. std::vector, Jak sugerowano w komentarzach do pytania):

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

Oto przykład na żywo .

Andy Prowl
źródło
9
Program operacyjny pyta, czy istnieje inne rozwiązanie poza szablonami.
Novak
1
@Adrian: Niestety nie ma innego rozwiązania, jeśli chcesz, aby twoja funkcja działała generalnie na tablicach dowolnego rozmiaru ...
Andy Prowl
1
Poprawnie: nie ma innego wyjścia. Ponieważ każda tablica std :: o innym rozmiarze jest innego typu, musisz napisać funkcję, która będzie działać na różnych typach. Dlatego szablony są rozwiązaniem dla std :: array.
bstamour
4
Piękną częścią korzystania z szablonu tutaj jest to, że możesz uczynić go jeszcze bardziej ogólnym, tak aby działał z dowolnym kontenerem sekwencji, a także standardowymi tablicami:template<typename C, typename M> void mulArray(C & arr, M multiplier) { /* same body */ }
Benjamin Lindley,
1
@BenjaminLindley: Oczywiście przy założeniu, że w ogóle może umieścić kod w nagłówku.
Nicol Bolas
28

Rozmiar arrayjest częścią typu , więc nie możesz zrobić tego, co chcesz. Jest kilka możliwości.

Preferowane byłoby wykonanie dwóch iteratorów:

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

Alternatywnie użyj vectorzamiast tablicy, co pozwala na przechowywanie rozmiaru w czasie wykonywania, a nie jako część jego typu:

void mulArray(std::vector<int>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}
Mark B.
źródło
1
Myślę, że to najlepsze rozwiązanie; jeśli masz zamiar przejść przez problem tworzenia szablonu, uczyń go całkowicie ogólnym z iteratorami, które pozwolą ci używać dowolnego kontenera (tablica, lista, wektor, nawet oldschoolowe wskaźniki C itp.) bez żadnych wad. Dzięki za podpowiedź.
Mark Lakata
9

EDYTOWAĆ

C ++ 20 zawiera wstępnie std::span

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

Oryginalna odpowiedź

To, czego potrzebujesz, to coś podobnego gsl::span, które jest dostępne w bibliotece wytycznych dotyczących obsługi opisanej w podstawowych wytycznych dotyczących języka C ++:

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

Implementację GSL zawierającą tylko nagłówek można znaleźć tutaj:

https://github.com/Microsoft/GSL

Dzięki gsl::spanmożesz to zrobić:

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

Problem std::arraypolega na tym, że jego rozmiar jest częścią jego typu, więc musiałbyś użyć szablonu, aby zaimplementować funkcję, która przyjmuje std::arraydowolny rozmiar.

gsl::spanz drugiej strony przechowuje jego rozmiar jako informacje w czasie wykonywania. Pozwala to na użycie jednej funkcji niebędącej szablonem do zaakceptowania tablicy o dowolnym rozmiarze. Przyjmuje również inne sąsiadujące kontenery:

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

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

Całkiem fajnie, co?

suncho
źródło
6

Spróbowałem poniżej i to po prostu zadziałało.

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

WYNIK :

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

piżmo
źródło
3
To nie jest poprawne C ++, ale raczej rozszerzenie. Te funkcje są szablonami, nawet bez template.
HolyBlackCat
1
Przyjrzałem się temu i wygląda na to, że auto foo(auto bar) { return bar * 2; }obecnie nie jest to poprawne C ++, mimo że kompiluje się w GCC7 z ustawioną flagą C ++ 17. Czytając tutaj , parametry funkcji zadeklarowane jako auto są częścią Concepts TS, która ostatecznie powinna być częścią C ++ 20.
Fibbles
Ostrzeżenie C26485
metablaster,
3

Oczywiście w C ++ 11 istnieje prosty sposób na napisanie funkcji, która pobiera tablicę std :: o znanym typie, ale nieznanym rozmiarze.

Jeśli nie jesteśmy w stanie przekazać funkcji rozmiaru tablicy, zamiast tego możemy przekazać adres pamięci, od której tablica się zaczyna, wraz z drugim adresem miejsca, w którym tablica się kończy. Później, wewnątrz funkcji, możemy użyć tych 2 adresów pamięci do obliczenia rozmiaru tablicy!

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

Wyjście na konsolę: 10, 20, 2, 4, 8

David M. Helmuth
źródło
1

Można to zrobić, ale wystarczy kilka kroków, aby zrobić to czysto. Najpierw napisz a, template classktóry reprezentuje zakres ciągłych wartości. Następnie przekaż templatewersję, która wie, jak duży arrayjest, do Implwersji, która przyjmuje ten ciągły zakres.

Na koniec zaimplementuj contig_rangewersję. Zauważ, że for( int& x: range )działa w przypadku contig_range, ponieważ zaimplementowałem begin()i, end()a wskaźniki to iteratory.

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

(nie testowane, ale projekt powinien działać).

Następnie w swoim .cpppliku:

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

Ma to tę wadę, że kod, który zapętla zawartość tablicy, nie wie (w czasie kompilacji), jak duża jest tablica, co może kosztować optymalizację. Ma tę zaletę, że implementacja nie musi znajdować się w nagłówku.

Uważaj na jawne konstruowanie a contig_range, ponieważ jeśli przekażesz a set, przyjmie on, że setdane są ciągłe, co jest fałszywe i będzie wykonywać niezdefiniowane zachowanie w każdym miejscu. Jedyne dwa stdkontenery, na których to działa, to vectori array(oraz tablice w stylu C, jak to się dzieje!). dequepomimo tego, że dostęp losowy nie jest ciągły (niebezpiecznie, jest ciągły w małych kawałkach!), listnie jest nawet blisko, a asocjacyjne (uporządkowane i nieuporządkowane) kontenery są równie nieciągłe.

Więc trzy konstruktory, które zaimplementowałem gdzie std::array, std::vectori tablice w stylu C, które w zasadzie obejmują podstawy.

Wykonawcza []jest łatwe, jak również i między for()i []to większość tego, co chcesz, by arraydla, prawda?

Yakk - Adam Nevraumont
źródło
Czy to nie jest po prostu przesunięcie szablonu w inne miejsce?
GManNickG,
@GManNickG w pewnym sensie. Nagłówek otrzymuje naprawdę krótką templatefunkcję bez żadnych szczegółów implementacji. ImplFunkcja nie jest templatefunkcją, a więc można szczęśliwie ukryć wdrożenie w .cpppliku wyboru. Jest to naprawdę prymitywny rodzaj wymazywania typu, w którym wyodrębniam możliwość iteracji po sąsiednich kontenerach do prostszej klasy, a następnie przepuszczam ją przez ... (chociaż multArrayImplprzyjmuje templatejako argument, nie jest templatesobą).
Yakk - Adam Nevraumont
Rozumiem, że ta klasa widoku tablicy / tablicy proxy jest czasami przydatna. Moją sugestią byłoby przekazanie początku / końca kontenera w konstruktorze, abyś nie musiał pisać konstruktora dla każdego kontenera. Nie pisałbym również '& * std :: begin (arr)', ponieważ dereferencja i branie adresu jest tu niepotrzebne, ponieważ std :: begin / std :: end już zwraca iterator.
Ricky65
@ Ricky65 Jeśli używasz iteratorów, musisz ujawnić implementację. Jeśli używasz wskaźników, nie. W &*dereferences iterator (które nie mogą być wskaźnikiem), to sprawia, że wskaźnik do adresu. W przypadku ciągłych danych w pamięci wskaźnik do begini wskaźnik do jednego z nich endsą również iteratorami dostępu swobodnego i są tego samego typu dla każdego ciągłego zakresu w typie T.
Yakk - Adam Nevraumont