Passaggio di un array std :: di dimensione sconosciuta a una funzione


97

In C ++ 11, come dovrei scrivere una funzione (o metodo) che accetta un array std :: di tipo noto ma dimensione sconosciuta?

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

Durante la mia ricerca ho trovato solo suggerimenti per utilizzare i modelli, ma quelli sembrano disordinati (definizioni dei metodi nell'intestazione) ed eccessivi per ciò che sto cercando di realizzare.

C'è un modo semplice per farlo funzionare, come si farebbe con semplici array in stile C?


1
Gli array non hanno limiti di controllo o sanno di che dimensione sono. Pertanto, devi avvolgerli in qualcosa o prenderne in considerazione l'uso std::vector.
Travis Pessetto

19
Se i modelli ti sembrano disordinati ed eccessivi, dovresti superare quella sensazione. Sono comuni in C ++.
Benjamin Lindley

Qualche motivo per non usarlo std::vectorcome consigliato da @TravisPessetto?
Cory Klein

2
Inteso. Se questo è un limite della loro natura, dovrò accettarlo. Il motivo per cui ho pensato di evitare std :: vector (che funziona alla grande per me), è che è allocato nell'heap. Poiché questi array saranno minuscoli e collegati in loop in ogni iterazione del programma, ho pensato che uno std :: array potrebbe funzionare un po 'meglio. Penso che userò un array in stile C quindi, il mio programma non è complesso.
Adrian

15
@Adrian Il tuo modo di pensare alla performance è completamente sbagliato. Non provare a fare micro ottimizzazioni prima ancora di avere un programma funzionale. E dopo che hai un programma, non indovinare cosa dovrebbe essere ottimizzato, invece lascia che un profiler ti dica quale parte del programma dovrebbe essere ottimizzata.
Paul Manta

Risposte:


86

C'è un modo semplice per farlo funzionare, come si farebbe con semplici array in stile C?

No. Non puoi davvero farlo a meno che non rendi la tua funzione un modello di funzione (o usi un altro tipo di contenitore, come un std::vector, come suggerito nei commenti alla domanda):

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

Ecco un esempio dal vivo .


7
L'OP chiede se esiste un'altra soluzione oltre ai modelli.
Novak

1
@Adrian: Sfortunatamente non c'è altra soluzione, se vuoi che la tua funzione funzioni genericamente su array di qualsiasi dimensione ...
Andy Prowl

1
Corretto: non c'è altro modo. Poiché ogni std :: array con una dimensione diversa è di un tipo diverso, è necessario scrivere una funzione che possa funzionare su tipi diversi. Quindi, i modelli sono la soluzione per std :: array.
bstamour

4
La parte bella dell'utilizzo di un modello qui è che puoi renderlo ancora più generico, in modo che funzioni con qualsiasi contenitore di sequenza, così come con gli array standard:template<typename C, typename M> void mulArray(C & arr, M multiplier) { /* same body */ }
Benjamin Lindley

1
@BenjaminLindley: Ovviamente, ciò presuppone che possa inserire il codice nell'intestazione.
Nicol Bolas

25

La dimensione del arrayè parte del tipo , quindi non puoi fare esattamente quello che vuoi. Ci sono un paio di alternative.

È preferibile prendere un paio di iteratori:

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

In alternativa, usa vectorinvece di array, che ti consente di memorizzare la dimensione in fase di esecuzione piuttosto che come parte del suo tipo:

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

1
Penso che questa sia la soluzione migliore; se hai intenzione di creare un modello, rendilo totalmente generico con iteratori che ti permetteranno di utilizzare qualsiasi contenitore (array, lista, vettore, persino i puntatori C della vecchia scuola, ecc.) senza svantaggi. Grazie per il suggerimento.
Mark Lakata

5

Ho provato di seguito e ha funzionato per me.

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

PRODUZIONE :

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


2
Questo non è un C ++ valido, ma piuttosto un'estensione. Queste funzioni sono modelli, anche senza template.
HolyBlackCat

Ho esaminato questo aspetto e sembra che auto foo(auto bar) { return bar * 2; }non sia attualmente valido C ++ anche se viene compilato in GCC7 con il flag C ++ 17 impostato. Dalla lettura qui , i parametri di funzione dichiarati come auto fanno parte di Concepts TS che dovrebbe eventualmente far parte di C ++ 20.
Fibbles


5

MODIFICARE

C ++ 20 include provvisoriamente std::span

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

Risposta originale

Quello che vuoi è qualcosa di simile gsl::span , che è disponibile nella libreria di supporto delle linee guida descritta nelle linee guida di base di C ++:

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

Puoi trovare un'implementazione di sola intestazione open source del GSL qui:

https://github.com/Microsoft/GSL

Con gsl::span, puoi farlo:

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

Il problema con std::array è che la sua dimensione fa parte del suo tipo, quindi dovresti usare un modello per implementare una funzione che accetta una std::arraydimensione arbitraria.

gsl::spand'altra parte memorizza le sue dimensioni come informazioni di runtime. Ciò consente di utilizzare una funzione non di modello per accettare un array di dimensioni arbitrarie. Accetterà anche altri contenitori contigui:

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

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

Piuttosto carino, eh?


3

Assolutamente, c'è un modo semplice in C ++ 11 per scrivere una funzione che accetta un array std :: di tipo noto, ma di dimensioni sconosciute.

Se non siamo in grado di passare la dimensione dell'array alla funzione, possiamo invece passare l'indirizzo di memoria di dove inizia l'array insieme a un secondo indirizzo di dove finisce l'array. Successivamente, all'interno della funzione, possiamo utilizzare questi 2 indirizzi di memoria per calcolare la dimensione dell'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;
}

Uscita alla console: 10, 20, 2, 4, 8


1

Questo può essere fatto, ma sono necessari alcuni passaggi per farlo in modo pulito. Innanzitutto, scrivi a template classche rappresenta un intervallo di valori contigui. Quindi inoltra una templateversione che sa quanto è grande arrayil fileImpl versione che prende questo intervallo contiguo.

Infine, implementa la contig_rangeversione. Nota che for( int& x: range )funziona per contig_range, perché ho implementato begin()e end()e i puntatori sono iteratori.

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 testato, ma il design dovrebbe funzionare).

Quindi, nel tuo .cppfile:

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

Questo ha lo svantaggio che il codice che esegue il ciclo sul contenuto dell'array non sa (in fase di compilazione) quanto è grande l'array, il che potrebbe costare l'ottimizzazione. Ha il vantaggio che l'implementazione non deve essere nell'intestazione.

Fai attenzione alla costruzione esplicita di a contig_range, poiché se lo passi a set, presumerà che i setdati siano contigui, il che è falso, e comporterà un comportamento indefinito ovunque. Gli unici due stdcontenitori su cui è garantito il funzionamento sono vectore array(e gli array in stile C, guarda caso!). dequenonostante sia l'accesso casuale non è contiguo (pericolosamente, è contiguo in piccoli pezzi!),list non è nemmeno vicino, ei contenitori associativi (ordinati e non ordinati) sono ugualmente non contigui.

Quindi i tre costruttori che ho implementato dove std::array,std::vector e in stile C gli array, che copre in pratica le basi.

Anche l'implementazione []è facile, e tra for()e []questo è la maggior parte di ciò che vuoi array, non è vero?


Non si tratta solo di spostare il modello da qualche altra parte?
GManNickG

@GManNickG sorta di. L'intestazione ottiene una templatefunzione molto breve con quasi nessun dettaglio di implementazione. La Implfunzione non è una templatefunzione, quindi puoi nascondere felicemente l'implementazione nel .cppfile di tua scelta. È un tipo di cancellazione del tipo davvero grezzo, in cui estraggo la capacità di iterare su contenitori contigui in una classe più semplice, e poi passarla attraverso ... (mentre multArrayImplprende templatecome argomento, non è un in templatesé).
Yakk - Adam Nevraumont

Capisco che questa classe di visualizzazione array / proxy array a volte sia utile. Il mio suggerimento sarebbe di passare l'inizio / la fine del contenitore nel costruttore in modo da non dover scrivere un costruttore per ogni contenitore. Inoltre, non scriverei '& * std :: begin (arr)' perché non è necessario annullare il riferimento e prendere l'indirizzo qui poiché std :: begin / std :: end restituisce già un iteratore.
Ricky65

@ Ricky65 Se usi gli iteratori, devi esporre l'implementazione. Se usi i puntatori, non lo fai. Il &*dereferenzia l'iteratore (che potrebbe non essere un puntatore), quindi crea un puntatore all'indirizzo. Per i dati di memoria contigui, anche il puntatore a begine il puntatore a uno dopo l'altro endsono iteratori ad accesso casuale e sono dello stesso tipo per ogni intervallo contiguo su un tipo T.
Yakk - Adam Nevraumont
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.