Passando um std :: array de tamanho desconhecido para uma função

97

No C ++ 11, como escrever uma função (ou método) que usa um std :: array de tipo conhecido, mas tamanho desconhecido?

// 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 minha pesquisa, encontrei apenas sugestões para usar modelos, mas essas parecem confusas (definições de método no cabeçalho) e excessivas para o que estou tentando realizar.

Existe uma maneira simples de fazer isso funcionar, como faria com arrays simples no estilo C?

Adrian
fonte
1
Os arrays não têm limites para verificar ou saber o tamanho deles. Portanto, você deve envolvê-los em algo ou considerar o uso de std::vector.
Travis Pessetto
19
Se os modelos parecem confusos e excessivos para você, você deve superar essa sensação. Eles são comuns em C ++.
Benjamin Lindley
Algum motivo para não usar std::vectorcomo recomenda @TravisPessetto?
Cory Klein
2
Entendido. Se isso é uma limitação de sua natureza, terei que aceitar isso. A razão pela qual pensei em evitar std :: vector (que funciona muito bem para mim), é que ele está alocado no heap. Uma vez que esses arrays serão minúsculos e serão repetidos em cada iteração do programa, pensei que um std :: array poderia ter um desempenho um pouco melhor. Acho que vou usar uma matriz de estilo C então, meu programa não é complexo.
Adrian de
15
@Adrian Sua maneira de pensar sobre desempenho está totalmente errada. Não tente fazer micro otimizações antes mesmo de ter um programa funcional. E depois que você tiver um programa, não adivinhe o que deve ser otimizado; em vez disso, deixe que um criador de perfil lhe diga que parte do programa deve ser otimizado.
Paul Manta

Respostas:

86

Existe uma maneira simples de fazer isso funcionar, como faria com arrays simples no estilo C?

Não. Você realmente não pode fazer isso a menos que torne sua função um modelo de função (ou use outro tipo de contêiner, como um std::vector, conforme sugerido nos comentários à pergunta):

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

Aqui está um exemplo ao vivo .

Andy Prowl
fonte
7
O OP pergunta se há alguma outra solução além dos modelos.
Novak de
1
@Adrian: Infelizmente não há outra solução, se você quiser que sua função funcione genericamente em arrays de qualquer tamanho ...
Andy Prowl
1
Correto: não há outra maneira. Como cada std :: array com um tamanho diferente é um tipo diferente, você precisa escrever uma função que possa funcionar em tipos diferentes. Portanto, os modelos são a solução para std :: array.
bstamour
4
A parte bonita de usar um modelo aqui é que você pode torná-lo ainda mais genérico, para que funcione com qualquer contêiner de sequência, bem como com matrizes padrão:template<typename C, typename M> void mulArray(C & arr, M multiplier) { /* same body */ }
Benjamin Lindley
1
@BenjaminLindley: Claro, isso pressupõe que ele pode colocar o código no cabeçalho.
Nicol Bolas
25

O tamanho de arrayfaz parte do tipo , então você não pode fazer exatamente o que deseja. Existem algumas alternativas.

O preferido seria usar um par de iteradores:

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

Como alternativa, use em vectorvez de array, que permite armazenar o tamanho no tempo de execução, em vez de como parte de seu tipo:

void mulArray(std::vector<int>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}
Mark B
fonte
1
Acho que essa é a solução superior; se você vai ter o trabalho de fazer um modelo, torne-o totalmente genérico com iteradores que permitirão que você use qualquer contêiner (array, lista, vetor, até mesmo ponteiros C da velha escola, etc) sem nenhuma desvantagem. Obrigado pela dica.
Mark Lakata
5

Eu tentei abaixo e funcionou para mim.

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

RESULTADO :

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

almíscar
fonte
2
Isso não é C ++ válido, mas sim uma extensão. Essas funções são modelos, mesmo sem template.
HolyBlackCat
Eu olhei para isso e parece que auto foo(auto bar) { return bar * 2; }não é C ++ válido no momento, embora seja compilado em GCC7 com o sinalizador C ++ 17 definido. Da leitura aqui , os parâmetros da função declarados como auto fazem parte do TS de conceitos que deve eventualmente fazer parte do C ++ 20.
Fibbles de
Aviso C26485
metablaster
5

EDITAR

C ++ 20 inclui provisoriamente std::span

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

Resposta Original

O que você quer é algo como gsl::span, que está disponível na Biblioteca de Suporte de Diretrizes descrita nas Diretrizes Básicas de C ++:

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

Você pode encontrar uma implementação somente de cabeçalho de código aberto da GSL aqui:

https://github.com/Microsoft/GSL

Com gsl::span, você pode fazer isso:

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

O problema com std::arrayé que seu tamanho é parte de seu tipo, então você teria que usar um modelo para implementar uma função std::arrayde tamanho arbitrário.

gsl::spanpor outro lado, armazena seu tamanho como informação de tempo de execução. Isso permite que você use uma função não modelo para aceitar uma matriz de tamanho arbitrário. Também aceitará outros contêineres contíguos:

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

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

Muito legal, hein?

suncho
fonte
3

Com certeza, existe uma maneira simples no C ++ 11 de escrever uma função que recebe um std :: array de tipo conhecido, mas de tamanho desconhecido.

Se não formos capazes de passar o tamanho do array para a função, então, em vez disso, podemos passar o endereço de memória de onde o array começa junto com um segundo endereço de onde o array termina. Posteriormente, dentro da função, podemos usar esses 2 endereços de memória para calcular o tamanho do 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;
}

Saída no console: 10, 20, 2, 4, 8

David M. Helmuth
fonte
1

Isso pode ser feito, mas são necessárias algumas etapas para fazer de forma limpa. Primeiro, escreva um template classque represente um intervalo de valores contíguos. Em seguida, encaminhe uma templateversão que saiba o quão grande arrayé para a Implversão que leva esse intervalo contíguo.

Finalmente, implemente a contig_rangeversão. Observe que for( int& x: range )funciona para contig_range, porque implementei begin()e end()e os ponteiros são iteradores.

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

(não testado, mas o design deve funcionar).

Então, em seu .cpparquivo:

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

Isso tem a desvantagem de que o código que faz um loop sobre o conteúdo do array não sabe (em tempo de compilação) o tamanho do array, o que pode custar a otimização. Tem a vantagem de que a implementação não precisa estar no cabeçalho.

Tenha cuidado ao construir explicitamente um contig_range, pois se você passá-lo um, setele assumirá que os setdados são contíguos, o que é falso, e terá um comportamento indefinido em todo o lugar. Os únicos dois stdcontêineres em que isso funciona são vectore array(e arrays de estilo C, por acaso!). dequeapesar de ser aleatório, o acesso não é contíguo (perigosamente, é contíguo em pequenos pedaços!), listnão é nem perto, e os containers associativos (ordenados e não ordenados) são igualmente não contíguos.

Assim, os três construtores I implementadas quando std::array, std::vectore C-estilo matrizes, que abrange basicamente as bases.

Implementação []é tão fácil assim, e entre for()e []que é mais do que você quer um arraypara, não é?

Yakk - Adam Nevraumont
fonte
Isso não é apenas deslocar o modelo para outro lugar?
GManNickG
@GManNickG mais ou menos. O cabeçalho obtém uma templatefunção realmente curta com quase nenhum detalhe de implementação. A Implfunção não é uma templatefunção e, portanto, você pode ocultar a implementação no .cpparquivo de sua escolha. É um tipo realmente grosseiro de eliminação de tipo, em que extraio a capacidade de iterar em contêineres contíguos para uma classe mais simples e, em seguida, transmito isso ... (embora multArrayImpltome a templatecomo argumento, não é a templatesi mesmo).
Yakk - Adam Nevraumont
Eu entendo que esta classe de exibição de matriz / proxy de matriz às vezes é útil. Minha sugestão seria passar o início / fim do contêiner no construtor para que você não tenha que escrever um construtor para cada contêiner. Além disso, eu não escreveria '& * std :: begin (arr)' como desreferenciamento e tomar o endereço é desnecessário aqui, pois std :: begin / std :: end já retorna um iterador.
Ricky65 de
@ Ricky65 Se você usa iteradores, precisa expor a implementação. Se você usa ponteiros, não o faz. O &*dereferencia o iterador (que pode não ser um ponteiro) e, em seguida, faz um ponteiro para o endereço. Para dados de memória contígua, o ponteiro para begine o ponteiro para um-passado-o endtambém são iteradores de acesso aleatório e são do mesmo tipo para cada intervalo contíguo sobre um tipo T.
Yakk - Adam Nevraumont