Pasando un std :: array de tamaño desconocido a una función

98

En C ++ 11, ¿cómo haría para escribir una función (o método) que tome una matriz std :: de tipo conocido pero de tamaño desconocido?

// 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 mi búsqueda, solo encontré sugerencias para usar plantillas, pero parecen desordenadas (definiciones de métodos en el encabezado) y excesivas para lo que estoy tratando de lograr.

¿Existe una forma sencilla de hacer que esto funcione, como lo haría con matrices simples de estilo C?

Adrian
fuente
1
Las matrices no tienen límites para verificar o saber qué tamaño son. Por lo tanto, debe envolverlos en algo o considerar su uso std::vector.
Travis Pessetto
20
Si las plantillas le parecen desordenadas y excesivas, debe superar ese sentimiento. Son comunes en C ++.
Benjamin Lindley
¿Alguna razón para no usar std::vectorcomo recomienda @TravisPessetto?
Cory Klein
2
Entendido. Si esta es una limitación de su naturaleza, tendré que aceptarla. La razón por la que pensé en evitar std :: vector (que funciona muy bien para mí) es que está asignado en el montón. Dado que estas matrices serán pequeñas y se repetirán en cada iteración del programa, pensé que una matriz std :: podría funcionar un poco mejor. Creo que usaré una matriz de estilo C entonces, mi programa no es complejo.
Adrian
15
@Adrian Tu forma de pensar sobre el rendimiento es completamente incorrecta. No intente realizar micro optimizaciones antes de tener un programa funcional. Y después de tener un programa, no adivine qué debe optimizarse, en su lugar deje que un generador de perfiles le diga qué parte del programa debe optimizarse.
Paul Manta

Respuestas:

86

¿Existe una forma sencilla de hacer que esto funcione, como lo haría con matrices simples de estilo C?

No. Realmente no puede hacer eso a menos que convierta su función en una plantilla de función (o use otro tipo de contenedor, como un std::vector, como se sugiere en los comentarios a la pregunta):

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

He aquí un ejemplo en vivo .

Andy Prowl
fuente
9
El OP pregunta si hay otra solución que no sean plantillas.
Novak
1
@Adrian: Desafortunadamente, no hay otra solución, si desea que su función funcione genéricamente en matrices de cualquier tamaño ...
Andy Prowl
1
Correcto: no hay otra forma. Dado que cada std :: array con un tamaño diferente es de un tipo diferente, necesita escribir una función que pueda funcionar en diferentes tipos. Por lo tanto, las plantillas son la solución para std :: array.
bstamour
4
La parte hermosa de usar una plantilla aquí es que puede hacer que esto sea aún más genérico, para que funcione con cualquier contenedor de secuencia, así como con matrices estándar:template<typename C, typename M> void mulArray(C & arr, M multiplier) { /* same body */ }
Benjamin Lindley
1
@BenjaminLindley: Por supuesto, eso supone que puede poner el código en el encabezado.
Nicol Bolas
27

El tamaño del arrayes parte del tipo , por lo que no puede hacer exactamente lo que quiere. Hay un par de alternativas.

Se prefiere tomar un par de iteradores:

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

Alternativamente, use en vectorlugar de matriz, que le permite almacenar el tamaño en tiempo de ejecución en lugar de como parte de su tipo:

void mulArray(std::vector<int>& arr, const int multiplier) {
    for(auto& e : arr) {
        e *= multiplier;
    }
}
Marca B
fuente
1
Creo que esta es la mejor solución; Si se va a tomar la molestia de hacer una plantilla, hágalo totalmente genérico con iteradores que le permitirán usar cualquier contenedor (matriz, lista, vector, incluso punteros C de la vieja escuela, etc.) sin inconvenientes. Gracias por la pista.
Mark Lakata
6

Lo intenté a continuación y funcionó para mí.

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

SALIDA:

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

almizcle
fuente
3
Esto no es C ++ válido, sino más bien una extensión. Estas funciones son plantillas, incluso sin ellas template.
HolyBlackCat
1
He investigado esto y parece que auto foo(auto bar) { return bar * 2; }actualmente no es C ++ válido a pesar de que se compila en GCC7 con el indicador C ++ 17 configurado. De la lectura aquí , los parámetros de función declarados como auto son parte de Concepts TS que eventualmente debería ser parte de C ++ 20.
Fibbles
Advertencia C26485
metablaster
5

EDITAR

C ++ 20 incluye tentativamente std::span

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

Respuesta original

Lo que desea es algo como gsl::span, que está disponible en la Biblioteca de soporte de pautas descrita en las Pautas principales de C ++:

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

Puede encontrar una implementación de solo encabezado de código abierto del GSL aquí:

https://github.com/Microsoft/GSL

Con gsl::span, puedes hacer esto:

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

El problema std::arrayes que su tamaño es parte de su tipo, por lo que tendría que usar una plantilla para implementar una función que tenga un std::arraytamaño arbitrario.

gsl::spanpor otro lado, almacena su tamaño como información en tiempo de ejecución. Esto le permite utilizar una función que no sea de plantilla para aceptar una matriz de tamaño arbitrario. También aceptará otros contenedores contiguos:

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

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

Bastante bien, ¿eh?

suncho
fuente
3

Absolutamente, hay una forma sencilla en C ++ 11 de escribir una función que toma una matriz std :: de tipo conocido, pero de tamaño desconocido.

Si no podemos pasar el tamaño de la matriz a la función, en su lugar, podemos pasar la dirección de memoria de donde comienza la matriz junto con una segunda dirección de donde termina la matriz. Más tarde, dentro de la función, podemos usar estas 2 direcciones de memoria para calcular el tamaño de la matriz.

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

Salida en consola: 10, 20, 2, 4, 8

David M. Helmuth
fuente
1

Esto se puede hacer, pero se necesitan algunos pasos para hacerlo de manera limpia. Primero, escriba un template classque represente un rango de valores contiguos. Luego, reenvíe una templateversión que sepa qué tan grande arrayes a la Implversión que toma este rango contiguo.

Finalmente, implemente la contig_rangeversión. Tenga en cuenta que for( int& x: range )funciona para contig_range, porque implementé begin()y end()y los punteros son 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 );
}

(no probado, pero el diseño debería funcionar).

Luego, en su .cpparchivo:

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

Esto tiene la desventaja de que el código que recorre el contenido de la matriz no sabe (en el momento de la compilación) qué tan grande es la matriz, lo que podría costar la optimización. Tiene la ventaja de que la implementación no tiene que estar en el encabezado.

Tenga cuidado al construir explícitamente a contig_range, ya que si lo pasa a set, asumirá que los setdatos son contiguos, lo cual es falso, y realizarán un comportamiento indefinido en todas partes. Los únicos dos stdcontenedores en los que se garantiza que esto funcionará son vectory array(¡y las matrices de estilo C, como sucede!). dequea pesar de ser el acceso aleatorio no es contiguo (peligrosamente, es contiguo en pequeños fragmentos), listni siquiera está cerca, y los contenedores asociativos (ordenados y desordenados) son igualmente no contiguos.

Así que los tres constructores implementé donde std::array, std::vectory al estilo de C matrices, que cubre básicamente las bases.

Ejecución []es fácil también, y entre for()y []que es más de lo que quiere una arraypara, ¿verdad?

Yakk - Adam Nevraumont
fuente
¿No es esto simplemente desplazar la plantilla a otro lugar?
GManNickG
@GManNickG algo así. El encabezado obtiene una templatefunción realmente corta sin casi detalles de implementación. La Implfunción no es una templatefunción, por lo que puede ocultar felizmente la implementación en el .cpparchivo de su elección. Es un tipo de borrado de tipo realmente crudo, donde extraigo la capacidad de iterar sobre contenedores contiguos en una clase más simple, y luego pasar eso a través ... (aunque multArrayImpltoma a templatecomo argumento, no es a templatesí mismo).
Yakk - Adam Nevraumont
Entiendo que esta clase de proxy de vista de matriz / matriz a veces es útil. Mi sugerencia sería pasar el inicio / final del contenedor en el constructor para no tener que escribir un constructor para cada contenedor. Además, no escribiría '& * std :: begin (arr)' ya que eliminar la referencia y tomar la dirección es innecesario aquí, ya que std :: begin / std :: end ya devuelve un iterador.
Ricky65
@ Ricky65 Si usa iteradores, debe exponer la implementación. Si usa punteros, no los usa. Los &*desreferencias el iterador (que puede no ser un puntero), luego hace un puntero a la dirección. Para datos de memoria contiguos, el puntero beginay el puntero a uno-pasado-el endtambién son iteradores de acceso aleatorio, y son del mismo tipo para cada rango contiguo sobre un tipo T.
Yakk - Adam Nevraumont