Como funciona o baseado em intervalo para matrizes simples?

87

No C ++ 11, você pode usar um baseado em intervalo for, que atua como o foreachde outras linguagens. Funciona mesmo com arrays C simples:

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

Como sabe quando parar? Ele funciona apenas com arrays estáticos que foram declarados no mesmo escopo em que forsão usados? Como você usaria isso forcom matrizes dinâmicas?

Paul Manta
fonte
10
Não há matrizes "dinâmicas" em C ou C ++ per se - existem tipos de matrizes e, em seguida, há ponteiros que podem ou não apontar para uma matriz ou um bloco de memória alocado dinamicamente que se comporta principalmente como uma matriz. Para qualquer array do tipo T [n], seu tamanho é codificado no tipo e pode ser acessado por for. Mas no momento em que o array se transforma em um ponteiro, as informações de tamanho são perdidas.
JohannesD
1
No seu exemplo, o número de elementos em numbersé sizeof(numbers)/sizeof(int), por exemplo.
JohannesD

Respostas:

57

Ele funciona para qualquer expressão cujo tipo seja uma matriz. Por exemplo:

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

Para uma explicação mais detalhada, se o tipo da expressão passada à direita de :for um tipo de array, o loop itera de ptrpara ptr + size( ptrapontando para o primeiro elemento do array, sizesendo a contagem de elementos do array).

Isso contrasta com os tipos definidos pelo usuário, que funcionam procurando begine endcomo membros se você passar um objeto de classe ou (se não houver nenhum membro chamado dessa forma) funções não membros. Essas funções produzirão os iteradores de início e fim (apontando para diretamente após o último elemento e o início da sequência, respectivamente).

Essa pergunta esclarece por que essa diferença existe.

Johannes Schaub - litb
fonte
8
Acho que a questão era como funciona, não quando funciona
ver
1
@sehe a pergunta continha vários '?' es. Um era "Funciona com ...?". Expliquei como e quando funciona.
Johannes Schaub - litb
8
@JohannesSchaub: Acho que a questão "como" aqui é como exatamente você obtém o tamanho de um objeto de um tipo de matriz em primeiro lugar (por causa da confusão de ponteiros vs. matrizes, nem quase todo mundo sabe que o tamanho de uma matriz é disponível para o programador.)
JohannesD
Eu acredito que ele procura por não-membro beginʻend . It just happens that std :: begin `std::enduse as funções de membro, e será usado se uma combinação melhor não estiver disponível.
Dennis Zickefoose
3
@Dennis no em Madrid foi decidido mudar isso e favorecer os membros inicial e final. Não favorecer os membros inicial e final causou ambigüidades que são difíceis de evitar.
Johannes Schaub - litb
44

Acho que a parte mais importante dessa questão é como C ++ sabe qual é o tamanho de um array (pelo menos eu queria saber quando encontrei essa questão).

C ++ sabe o tamanho de um array, porque é parte da definição do array - é o tipo da variável. Um compilador precisa saber o tipo.

Uma vez que C ++ 11 std::extentpode ser usado para obter o tamanho de uma matriz:

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

Claro, isso não faz muito sentido, porque você tem que fornecer explicitamente o tamanho na primeira linha, que você obtém na segunda linha. Mas você também pode usardecltype e fica mais interessante:

char v[] { 'A', 'B', 'C', 'D' };
int size2{ std::extent< decltype(v) >::value };
std::cout << "Array size: " << size2 << std::endl;
psur
fonte
6
Isso é de fato o que eu estava perguntando originalmente. :)
Paul Manta
19

De acordo com o C ++ Working Draft mais recente (n3376), a instrução range for é equivalente ao seguinte:

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

Portanto, ele sabe como parar da mesma forma que um forloop regular que usa iteradores.

Acho que você pode estar procurando algo como o seguinte para fornecer uma maneira de usar a sintaxe acima com matrizes que consistem em apenas um ponteiro e tamanho (matrizes 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;
};

Este modelo de classe pode então ser usado para criar um intervalo, sobre o qual você pode iterar usando o novo intervalo para sintaxe. Estou usando isso para percorrer todos os objetos de animação em uma cena que é importada usando uma biblioteca que retorna apenas um ponteiro para uma matriz e um tamanho como valores separados.

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

Essa sintaxe é, na minha opinião, muito mais clara do que você usaria std::for_eachou um forloop simples .

Conceder
fonte
3

Ele sabe quando parar porque conhece os limites dos arrays estáticos.

Não tenho certeza do que você quer dizer com "matrizes dinâmicas", em qualquer caso, se não iterar sobre matrizes estáticas, informalmente, o compilador procura os nomes begine endno escopo da classe do objeto que você itera ou olha -se para begin(range)e end(range)usando de pesquisa e os usa como iterators dependente de argumento.

Para obter mais informações, no padrão C ++ 11 (ou rascunho público), "6.5.4 A fordeclaração baseada em intervalo ", pg.145

calafrio
fonte
4
Um "array dinâmico" seria aquele criado com new[]. Nesse caso, você tem apenas um ponteiro sem indicação do tamanho, então não há como o baseado forem intervalo trabalhar com ele.
Mike Seymour
Minha resposta inclui uma matriz dinâmica cujo tamanho (4) é conhecido em tempo de compilação, mas não sei se essa interpretação de "matriz dinâmica" é o que o questionador pretendia.
Johannes Schaub - litb
3

Como funciona o baseado em intervalo para matrizes simples?

Isso deve ser lido como " Diga-me o que faz um ranged-for (com matrizes)? "

Responderei supondo que - tome o seguinte exemplo usando matrizes aninhadas:

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

for (auto &pl : ia)

Versão do texto:

iaé uma matriz de matrizes ("matriz aninhada"), contendo [3]matrizes, cada uma contendo [4]valores. O exemplo acima faz um loop iapor seu 'intervalo' primário ( [3]) e, portanto, faz um loop nos [3]tempos. Cada circuito produz um dos ia's [3]valores primários a partir do primeiro e terminando com o último - Uma matriz contendo [4]os valores.

  • Primeiro loop: pligual a {1,2,3,4}- uma matriz
  • Segundo loop: pligual a {5,6,7,8}- uma matriz
  • Terceiro loop: pligual a {9,10,11,12}- uma matriz

Antes de explicarmos o processo, aqui estão alguns lembretes amigáveis ​​sobre matrizes:

  • Matrizes são interpretadas como ponteiros para seu primeiro valor - Usar uma matriz sem qualquer iteração retorna o endereço do primeiro valor
  • pl devo ser uma referência porque não podemos copiar matrizes
  • Com matrizes, quando você adiciona um número ao próprio objeto de matriz, ele avança muitas vezes e 'aponta' para a entrada equivalente - Se nfor o número em questão, então ia[n]é o mesmo que *(ia+n)(Estamos desreferenciando o endereço das nentradas forward) e ia+né o mesmo que &ia[n](estamos obtendo o endereço dessa entrada na matriz).

Aqui está o que está acontecendo:

  • Em cada loop, plé definido como uma referência a ia[n], nigualando a contagem do loop atual a partir de 0. Então, plestá ia[0]na primeira rodada, na segunda é ia[1], e assim por diante. Ele recupera o valor por meio de iteração.
  • O loop continua enquanto ia+nfor menor que end(ia).

... E é isso.

Na verdade, é apenas uma maneira simplificada de escrever isso :

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];

Se a sua matriz não estiver aninhada, esse processo se torna um pouco mais simples , pois não é necessária uma referência , porque o valor iterado não é uma matriz, mas um 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];

Algumas informações adicionais

E se não quisermos usar a autopalavra - chave ao criar pl? Qual seria a aparência disso?

No exemplo a seguir, plrefere-se a um array of four integers. Em cada loop plé dado o valor ia[n]:

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

E ... É assim que funciona, com informações adicionais para afastar qualquer confusão. É apenas um forloop 'abreviado' que conta automaticamente para você, mas não tem uma maneira de recuperar o loop atual sem fazer isso manualmente.

Super gata
fonte
@Andy 9 em cada 10 vezes o título é o que corresponde nas pesquisas do Google / qualquer coisa - O título pergunta como isso funciona? , não quando sabe quando parar? . Mesmo assim, a questão subjacente implícita é abordada nesta resposta até certo ponto e continua a responder para qualquer pessoa que esteja procurando a outra resposta. Perguntas de sintaxe como essas devem ter títulos formulados para que uma resposta possa ser escrita usando apenas isso, porque essa é toda a informação de que o pesquisador precisa para encontrar a pergunta. Você certamente não está errado - A pergunta não tem o título como deveria.
Super Cat
0

Alguns exemplos de código para demonstrar a diferença entre matrizes em Stack e matrizes em 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 Cubed
fonte