Meneruskan larik std :: dengan ukuran yang tidak diketahui ke suatu fungsi

104

Dalam C ++ 11, bagaimana saya akan menulis sebuah fungsi (atau metode) yang mengambil std :: array dari tipe yang diketahui tetapi ukurannya tidak diketahui?

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

Selama pencarian saya, saya hanya menemukan saran untuk menggunakan template, tetapi itu tampak berantakan (definisi metode di header) dan berlebihan untuk apa yang saya coba capai.

Apakah ada cara sederhana untuk membuat ini berfungsi, seperti yang dilakukan orang dengan array C-style biasa?

Adrian
sumber
1
Array tidak memiliki batas pemeriksaan atau tahu ukurannya. Karena itu, Anda harus membungkusnya dengan sesuatu atau mempertimbangkan penggunaannya std::vector.
Travis Pessetto
20
Jika templat tampak berantakan dan berlebihan bagi Anda, Anda harus mengatasi perasaan itu. Mereka biasa di C ++.
Benjamin Lindley
Ada alasan untuk tidak menggunakan std::vectorseperti yang direkomendasikan @TravisPessetto?
Cory Klein
2
Dimengerti. Jika ini adalah batasan sifat mereka, saya harus menerimanya. Alasan saya berpikir untuk menghindari std :: vector (yang berfungsi dengan baik untuk saya), adalah karena dialokasikan di heap. Karena array ini akan menjadi kecil dan berulang di setiap iterasi program, saya pikir std :: array mungkin tampil sedikit lebih baik. Saya pikir saya akan menggunakan array C-style kemudian, program saya tidak rumit.
Adrian
15
@Adrian Cara berpikir Anda tentang kinerja sepenuhnya salah. Jangan mencoba melakukan pengoptimalan mikro bahkan sebelum Anda memiliki program fungsional. Dan setelah Anda memiliki program, jangan menebak - nebak apa yang harus dioptimalkan, alih-alih biarkan profiler memberi tahu Anda bagian mana dari program yang harus dioptimalkan.
Paul Manta

Jawaban:

92

Apakah ada cara sederhana untuk membuat ini berfungsi, seperti yang dilakukan orang dengan array C-style biasa?

Tidak. Anda benar-benar tidak dapat melakukan itu kecuali Anda membuat fungsi Anda sebagai templat fungsi (atau menggunakan wadah jenis lain, seperti std::vector, seperti yang disarankan dalam komentar pada pertanyaan):

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

Berikut adalah contoh langsungnya .

Andy Prowl
sumber
9
OP menanyakan apakah ada solusi lain selain template.
Novak
1
@Adrian: Sayangnya tidak ada solusi lain, jika Anda ingin fungsi Anda berfungsi secara umum pada array dengan ukuran berapa pun ...
Andy Prowl
1
Benar: tidak ada cara lain. Karena setiap std :: array dengan ukuran berbeda adalah tipe yang berbeda, Anda perlu menulis fungsi yang dapat bekerja pada tipe yang berbeda. Karenanya, template adalah solusi untuk std :: array.
Bstamour
4
Bagian yang indah tentang menggunakan template di sini adalah Anda dapat membuatnya lebih umum, sehingga berfungsi dengan container urutan apa pun, serta array standar:template<typename C, typename M> void mulArray(C & arr, M multiplier) { /* same body */ }
Benjamin Lindley
1
@BenjaminLindley: Tentu saja, itu mengasumsikan bahwa dia dapat meletakkan kode di header sama sekali.
Nicol Bolas
29

Ukurannya arrayadalah bagian dari tipe , jadi Anda tidak dapat melakukan apa yang Anda inginkan. Ada beberapa alternatif.

Lebih disukai mengambil sepasang iterator:

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

Alternatifnya, gunakan vectorsebagai ganti array, yang memungkinkan Anda menyimpan ukuran saat runtime bukan sebagai bagian dari tipenya:

void mulArray(std::vector<int>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}
Mark B
sumber
2
Saya pikir ini adalah solusi terbaik; jika Anda akan mengalami kesulitan dalam membuat template, buatlah itu benar-benar generik dengan iterator yang akan memungkinkan Anda menggunakan wadah apa pun (array, daftar, vektor, bahkan pointer C jadul, dll) tanpa kerugian. Terima kasih atas petunjuknya.
Mark Lakata
9

EDIT

C ++ 20 untuk sementara menyertakan std::span

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

Jawaban Asli

Yang Anda inginkan adalah sesuatu seperti itu gsl::span, yang tersedia di Guideline Support Library yang dijelaskan dalam C ++ Core Guidelines:

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

Anda dapat menemukan implementasi GSL hanya-header open source di sini:

https://github.com/Microsoft/GSL

Dengan gsl::span, Anda dapat melakukan ini:

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

Masalahnya std::arrayadalah ukurannya adalah bagian dari tipenya, jadi Anda harus menggunakan template untuk mengimplementasikan fungsi yang mengambil std::arrayukuran sembarang.

gsl::spandi sisi lain menyimpan ukurannya sebagai informasi run-time. Ini memungkinkan Anda menggunakan satu fungsi non-template untuk menerima larik dengan ukuran sembarang. Ini juga akan menerima wadah bersebelahan lainnya:

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

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

Cukup keren, ya?

suncho
sumber
6

Saya mencoba di bawah ini dan itu berhasil untuk saya.

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

OUTPUT:

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

musk
sumber
3
Ini bukan C ++ yang valid, melainkan ekstensi. Fungsi ini adalah template, bahkan tanpa template.
HolyBlackCat
1
Saya telah auto foo(auto bar) { return bar * 2; }memeriksa ini dan tampaknya C ++ saat ini tidak valid meskipun dikompilasi di GCC7 dengan set flag C ++ 17. Dari membaca di sini , parameter fungsi yang dideklarasikan sebagai auto adalah bagian dari TS Konsep yang pada akhirnya harus menjadi bagian dari C ++ 20.
Fibbles
Peringatan C26485
metablaster
3

Tentu saja, ada cara sederhana di C ++ 11 untuk menulis fungsi yang menggunakan std :: array dengan tipe yang diketahui, tetapi ukurannya tidak diketahui.

Jika kita tidak dapat meneruskan ukuran array ke fungsi tersebut, maka sebagai gantinya, kita dapat mengirimkan alamat memori tempat array dimulai bersama dengan alamat kedua tempat array berakhir. Nanti, di dalam fungsi tersebut, kita dapat menggunakan 2 alamat memori ini untuk menghitung ukuran array!

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

Output di Konsol: 10, 20, 2, 4, 8

David M. Helmuth
sumber
1

Ini bisa dilakukan, tetapi perlu beberapa langkah untuk melakukannya dengan rapi. Pertama, tulis a template classyang mewakili rentang nilai yang berdekatan. Kemudian teruskan templateversi yang mengetahui seberapa besar arrayis ke Implversi yang mengambil rentang yang berdekatan ini.

Terakhir, terapkan contig_rangeversinya. Perhatikan itu for( int& x: range )berfungsi untuk contig_range, karena saya menerapkan begin()dan end()dan penunjuk adalah iterator.

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

(tidak diuji, tetapi desain harus berfungsi).

Kemudian, di .cppfile Anda :

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

Ini memiliki kelemahan bahwa kode yang mengulang konten array tidak tahu (pada waktu kompilasi) seberapa besar array itu, yang dapat memerlukan biaya pengoptimalan. Keuntungannya adalah implementasinya tidak harus di header.

Berhati-hatilah saat membuat a secara eksplisit contig_range, karena jika Anda meneruskannya, setitu akan menganggap bahwa setdatanya bersebelahan, yang salah, dan melakukan perilaku tidak terdefinisi di semua tempat. Hanya dua stdcontainer yang dijamin akan berfungsi adalah vectordan array(dan array C-style, saat terjadi!). dequemeskipun akses acak tidak bersebelahan (berbahaya, itu bersebelahan dalam potongan kecil!), listbahkan tidak dekat, dan wadah asosiatif (teratur dan tidak berurutan) sama-sama tidak bersebelahan.

Jadi tiga konstruktor yang saya implementasikan di mana std::array, std::vectordan array gaya-C, yang pada dasarnya mencakup pangkalan.

Menerapkannya []juga mudah, dan di antara for()dan []itulah yang paling Anda inginkan array, bukan?

Yakk - Adam Nevraumont
sumber
Bukankah ini hanya mengganti template ke tempat lain?
GManNickG
@GManNickG semacam. Header mendapatkan templatefungsi yang sangat pendek tanpa detail implementasi. The Implfungsi bukan templatefungsi, dan sehingga Anda dapat dengan senang hati menyembunyikan implementasi dalam .cppfile pilihan Anda. Ini adalah jenis penghapusan tipe yang sangat kasar, di mana saya mengekstrak kemampuan untuk mengulang kontainer yang berdekatan ke dalam kelas yang lebih sederhana, dan kemudian meneruskannya ... (sementara multArrayImplmengambil templatesebagai argumen, itu bukan templatedirinya sendiri).
Yakk - Adam Nevraumont
Saya memahami bahwa kelas proxy tampilan / array array ini terkadang berguna. Saran saya adalah meneruskan awal / akhir wadah di konstruktor sehingga Anda tidak perlu menulis konstruktor untuk setiap wadah. Saya juga tidak akan menulis '& * std :: begin (arr)' sebagai dereferencing dan mengambil alamat tidak diperlukan di sini karena std :: begin / std :: end sudah mengembalikan iterator.
Ricky65
@ Ricky65 Jika Anda menggunakan iterator, Anda perlu mengekspos implementasinya. Jika Anda menggunakan pointer, Anda tidak. The &*dereferences iterator (yang mungkin tidak pointer), kemudian membuat pointer ke alamat. Untuk data memori yang berdekatan, penunjuk ke begindan penunjuk ke one-past-the endjuga merupakan iterator akses acak, dan mereka adalah tipe yang sama untuk setiap rentang yang berdekatan di atas suatu jenis T.
Yakk - Adam Nevraumont