¿Cómo funciona el basado en rango para arreglos simples?

87

En C ++ 11 puede usar un rango basado en for, que actúa como el foreachde otros lenguajes. Funciona incluso con matrices C simples:

int numbers[] = { 1, 2, 3, 4, 5 };
for (int& n : numbers) {
    n *= 2;
}

¿Cómo sabe cuándo detenerse? ¿Solo funciona con matrices estáticas que se han declarado en el mismo ámbito en el que forse utiliza? ¿Cómo usarías esto forcon matrices dinámicas?

Paul Manta
fuente
10
No hay matrices "dinámicas" en C o C ++ per se; hay tipos de matriz y luego hay punteros que pueden o no apuntar a una matriz o un bloque de memoria asignado dinámicamente que se comporta principalmente como una matriz. Para cualquier arreglo de tipo T [n], su tamaño está codificado en el tipo y se puede acceder a él for. Pero en el momento en que esa matriz se convierte en un puntero, la información de tamaño se pierde.
JohannesD
1
En su ejemplo, el número de elementos en numberses sizeof(numbers)/sizeof(int), por ejemplo.
JohannesD

Respuestas:

57

Funciona para cualquier expresión cuyo tipo sea una matriz. Por ejemplo:

int (*arraypointer)[4] = new int[1][4]{{1, 2, 3, 4}};
for(int &n : *arraypointer)
  n *= 2;
delete [] arraypointer;

Para una explicación más detallada, si el tipo de expresión pasada a la derecha de :es un tipo de matriz, entonces el bucle itera de ptra ptr + size( ptrapuntando al primer elemento de la matriz, que sizees el recuento de elementos de la matriz).

Esto contrasta con los tipos definidos por el usuario, que funcionan mirando hacia arriba beginy endcomo miembros si pasa un objeto de clase o (si no hay miembros llamados de esa manera) funciones no miembros. Esas funciones producirán los iteradores de inicio y final (apuntando directamente después del último elemento y el comienzo de la secuencia, respectivamente).

Esta pregunta aclara por qué existe esa diferencia.

Johannes Schaub - litb
fuente
8
Creo que la pregunta era cómo funciona, no cuándo funciona
Consulte el
1
@sehe la pregunta contenía múltiples '?' es. Uno era "¿Funciona con ...?". Expliqué cómo y cuándo funciona.
Johannes Schaub - litb
8
@JohannesSchaub: Creo que el problema de "cómo" aquí es cómo se obtiene exactamente el tamaño de un objeto de un tipo de matriz en primer lugar (debido a la confusión de punteros frente a matrices, no casi todos saben que el tamaño de una matriz es disponible para el programador.)
JohannesD
Creo que solo busca que no sea miembro begin`end . It just happens that std :: begin `std::enduse las funciones miembro, y se usará si no hay una coincidencia mejor disponible.
Dennis Zickefoose
3
@Dennis no en Madrid se decidió cambiar eso y favorecer a los miembros iniciales y finales. No favorecer a los miembros iniciales y finales provocó ambigüedades que son difíciles de evitar.
Johannes Schaub - litb
44

Creo que la parte más importante de esta pregunta es cómo C ++ sabe cuál es el tamaño de una matriz (al menos quería saberlo cuando encontré esta pregunta).

C ++ conoce el tamaño de una matriz porque es parte de la definición de la matriz, es el tipo de variable. Un compilador debe conocer el tipo.

Dado que C ++ 11 std::extentse puede utilizar para obtener el tamaño de una matriz:

int size1{ std::extent< char[5] >::value };
std::cout << "Array size: " << size1 << std::endl;

Por supuesto, esto no tiene mucho sentido, porque debe proporcionar explícitamente el tamaño en la primera línea, que luego obtiene en la segunda línea. Pero también puedes usar decltypey luego se vuelve más interesante:

char v[] { 'A', 'B', 'C', 'D' };
int size2{ std::extent< decltype(v) >::value };
std::cout << "Array size: " << size2 << std::endl;
psur
fuente
6
De hecho, esto es sobre lo que estaba preguntando originalmente. :)
Paul Manta
19

Según el último borrador de trabajo de C ++ (n3376), la declaración de rango es equivalente a lo siguiente:

{
    auto && __range = range-init;
    for (auto __begin = begin-expr,
              __end = end-expr;
            __begin != __end;
            ++__begin) {
        for-range-declaration = *__begin;
        statement
    }
}

Por lo tanto, sabe cómo detenerse de la misma manera que lo hace un forbucle regular que usa iteradores.

Creo que puede estar buscando algo como lo siguiente para proporcionar una forma de usar la sintaxis anterior con matrices que constan solo de un puntero y tamaño (matrices dinámicas):

template <typename T>
class Range
{
public:
    Range(T* collection, size_t size) :
        mCollection(collection), mSize(size)
    {
    }

    T* begin() { return &mCollection[0]; }
    T* end () { return &mCollection[mSize]; }

private:
    T* mCollection;
    size_t mSize;
};

Esta plantilla de clase se puede usar para crear un rango, sobre el cual puede iterar usando la nueva sintaxis de rango . Estoy usando esto para ejecutar todos los objetos de animación en una escena que se importa usando una biblioteca que solo devuelve un puntero a una matriz y un tamaño como valores separados.

for ( auto pAnimation : Range<aiAnimation*>(pScene->mAnimations, pScene->mNumAnimations) )
{
    // Do something with each pAnimation instance here
}

Esta sintaxis es, en mi opinión, mucho más clara de lo que obtendría usando std::for_eachun forbucle simple .

Conceder
fuente
3

Sabe cuándo detenerse porque conoce los límites de las matrices estáticas.

No estoy seguro de qué quiere decir con "matrices dinámicas", en cualquier caso, si no itera sobre matrices estáticas, informalmente, el compilador busca los nombres beginy enden el alcance de la clase del objeto sobre el que itera, o busca para begin(range)y end(range)usar la búsqueda dependiente de argumentos y los usa como iteradores.

Para obtener más información, en el estándar C ++ 11 (o borrador público del mismo), "6.5.4 La fordeclaración basada en rangos ", pág.145

frío
fuente
4
Una "matriz dinámica" sería una creada con new[]. En ese caso, solo tiene un puntero sin indicación del tamaño, por lo que no hay forma de que el basado foren rango funcione con él.
Mike Seymour
Mi respuesta incluye una matriz dinámica cuyo tamaño (4) se conoce en tiempo de compilación, pero no sé si esa interpretación de "matriz dinámica" es lo que pretendía el interrogador.
Johannes Schaub - litb
3

¿Cómo funciona el basado en rango para arreglos simples?

¿Es eso para leer como, " Dime qué hace un rango (con matrices)? "

Responderé asumiendo eso: tome el siguiente ejemplo usando matrices anidadas:

int ia[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};

for (auto &pl : ia)

Versión de texto:

iaes una matriz de matrices ("matriz anidada"), que contiene [3]matrices, y cada una contiene [4]valores. El ejemplo anterior recorre iasu 'rango' primario ( [3]) y, por lo tanto, repite [3]tiempos. Cada ciclo produce uno de ialos [3]valores primarios comenzando por el primero y terminando con el último: una matriz que contiene [4]valores.

  • Primer ciclo: pligual a {1,2,3,4}- Una matriz
  • Segundo ciclo: pligual a {5,6,7,8}- Una matriz
  • Tercer ciclo: pligual a {9,10,11,12}- Una matriz

Antes de explicar el proceso, aquí hay algunos recordatorios amigables sobre las matrices:

  • Las matrices se interpretan como punteros a su primer valor: el uso de una matriz sin ninguna iteración devuelve la dirección del primer valor
  • pl debe ser una referencia porque no podemos copiar matrices
  • Con las matrices, cuando agrega un número al objeto de matriz en sí, avanza tantas veces y 'apunta' a la entrada equivalente: si nes el número en cuestión, entonces ia[n]es lo mismo que *(ia+n)(estamos desreferenciando la dirección que son nentradas forward), y ia+nes lo mismo que &ia[n](Estamos obteniendo la dirección de esa entrada en la matriz).

Esto es lo que está pasando:

  • En cada bucle, plse establece como una referencia a ia[n], nigualando el recuento del bucle actual comenzando desde 0. Entonces, plestá ia[0]en la primera ronda, en la segunda está ia[1], y así sucesivamente. Recupera el valor mediante iteración.
  • El bucle continúa mientras ia+nsea ​​menor que end(ia).

... Y eso es todo.

En realidad, es solo una forma simplificada de escribir esto :

int ia[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
for (int n = 0; n != 3; ++n)
  auto &pl = ia[n];

Si su matriz no está anidada, entonces este proceso se vuelve un poco más simple en el sentido de que no se necesita una referencia , porque el valor iterado no es una matriz sino un valor 'normal':

 int ib[3] = {1,2,3};

 // short
 for (auto pl : ib)
   cout << pl;

 // long
 for (int n = 0; n != 3; ++n)
   cout << ib[n];

Alguna información adicional

¿Qué pasa si no queremos usar la autopalabra clave al crear pl? Como se veria eso?

En el siguiente ejemplo, se plrefiere a un array of four integers. En cada bucle plse le da el valor ia[n]:

int ia[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};
for (int (&pl)[4] : ia)

Y ... Así es como funciona, con información adicional para eliminar cualquier confusión. Es solo un forbucle 'taquigráfico' que cuenta automáticamente para usted, pero carece de una forma de recuperar el bucle actual sin hacerlo manualmente.

Super gato
fuente
@Andy 9 de cada 10 veces el título es lo que coincide en Google / cualquier búsqueda. El título pregunta ¿cómo funcionan? , no, ¿ cuándo sabe cuándo detenerse? . Aun así, la pregunta subyacente implícita se cubre en esta respuesta hasta cierto punto, y continúa respondiendo para cualquiera que busque la otra respuesta. Las preguntas de sintaxis como estas deben tener títulos redactados de manera que se pueda escribir una respuesta solo con eso porque esa es toda la información que el buscador necesita para encontrar la pregunta. Ciertamente no te equivocas: la pregunta no está titulada como debería.
Super Cat
0

Algunos ejemplos de código para demostrar la diferencia entre las matrices en Stack y las matrices en Heap


/**
 * Question: Can we use range based for built-in arrays
 * Answer: Maybe
 * 1) Yes, when array is on the Stack
 * 2) No, when array is the Heap
 * 3) Yes, When the array is on the Stack,
 *    but the array elements are on the HEAP
 */
void testStackHeapArrays() {
  int Size = 5;
  Square StackSquares[Size];  // 5 Square's on Stack
  int StackInts[Size];        // 5 int's on Stack
  // auto is Square, passed as constant reference
  for (const auto &Sq : StackSquares)
    cout << "StackSquare has length " << Sq.getLength() << endl;
  // auto is int, passed as constant reference
  // the int values are whatever is in memory!!!
  for (const auto &I : StackInts)
    cout << "StackInts value is " << I << endl;

  // Better version would be: auto HeapSquares = new Square[Size];
  Square *HeapSquares = new Square[Size];   // 5 Square's on Heap
  int *HeapInts = new int[Size];            // 5 int's on Heap

  // does not compile,
  // *HeapSquares is a pointer to the start of a memory location,
  // compiler cannot know how many Square's it has
  // for (auto &Sq : HeapSquares)
  //    cout << "HeapSquare has length " << Sq.getLength() << endl;

  // does not compile, same reason as above
  // for (const auto &I : HeapInts)
  //  cout << "HeapInts value is " << I << endl;

  // Create 3 Square objects on the Heap
  // Create an array of size-3 on the Stack with Square pointers
  // size of array is known to compiler
  Square *HeapSquares2[]{new Square(23), new Square(57), new Square(99)};
  // auto is Square*, passed as constant reference
  for (const auto &Sq : HeapSquares2)
    cout << "HeapSquare2 has length " << Sq->getLength() << endl;

  // Create 3 int objects on the Heap
  // Create an array of size-3 on the Stack with int pointers
  // size of array is known to compiler
  int *HeapInts2[]{new int(23), new int(57), new int(99)};
  // auto is int*, passed as constant reference
  for (const auto &I : HeapInts2)
    cout << "HeapInts2 has value " << *I << endl;

  delete[] HeapSquares;
  delete[] HeapInts;
  for (const auto &Sq : HeapSquares2) delete Sq;
  for (const auto &I : HeapInts2) delete I;
  // cannot delete HeapSquares2 or HeapInts2 since those arrays are on Stack
}
Yip en cubos
fuente