Jak działa na podstawie zakresu dla zwykłych tablic?

88

W C ++ 11 możesz użyć zakresu opartego na zakresie for, który działa jak foreachinne języki. Działa nawet ze zwykłymi tablicami C:

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

Skąd wie, kiedy przestać? Czy działa tylko z tablicami statycznymi, które zostały zadeklarowane w tym samym zakresie, w którym forjest używany? Jak użyłbyś tego forz dynamicznymi tablicami?

Paul Manta
źródło
10
Nie ma tablic „dynamicznych” w C lub C ++ per se - istnieją typy tablic, a następnie są wskaźniki, które mogą, ale nie muszą wskazywać na tablicę lub dynamicznie przydzielany blok pamięci, który zachowuje się głównie jak tablica. W przypadku dowolnej tablicy typu T [n] jej rozmiar jest zakodowany w typie i można uzyskać do niego dostęp for. Ale w momencie, gdy tablica rozpada się na wskaźnik, informacja o rozmiarze zostaje utracona.
JohannesD,
1
W twoim przykładzie liczba elementów w numbersto sizeof(numbers)/sizeof(int)na przykład.
JohannesD

Odpowiedzi:

57

Działa dla każdego wyrażenia, którego typem jest tablica. Na przykład:

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

Aby uzyskać bardziej szczegółowe wyjaśnienie, jeśli typ wyrażenia przekazanego po prawej stronie :jest typem tablicowym, pętla wykonuje iterację od ptrdo ptr + size( ptrwskazując na pierwszy element tablicy, sizebędący liczbą elementów tablicy).

Jest to w przeciwieństwie do typów zdefiniowanych przez użytkownika, które działają na zasadzie wyszukiwania w górę begini endjako członkowie, jeśli przekazujesz obiekt klasy lub (jeśli nie ma tak nazwanych elementów członkowskich) funkcje niebędące członkami. Te funkcje dadzą iteratory początku i końca (wskazujące odpowiednio bezpośrednio po ostatnim elemencie i początku sekwencji).

To pytanie wyjaśnia, dlaczego istnieje taka różnica.

Johannes Schaub - litb
źródło
8
Myślę, że pytanie było , jak to działa, a nie po to działa
sehe
1
@sehe pytanie zawierało wiele znaków „?”. Jednym z nich było „Czy to działa z…?”. Wyjaśniłem, jak i kiedy to działa.
Johannes Schaub - litb
8
@JohannesSchaub: Myślę, że problemem "jak" jest tutaj to, jak dokładnie uzyskujesz rozmiar obiektu typu tablicy (z powodu pomyłki wskaźników i tablic, nie prawie każdy wie, że rozmiar tablicy jest dostępne dla programisty.)
JohannesD,
Uważam, że wyszukuje on tylko nie-składowe begin`end . It just happens that std :: begin `std::endużywa funkcji składowych i zostanie użyte, jeśli lepsze dopasowanie nie jest dostępne.
Dennis Zickefoose
3
@Dennis no w Madrycie postanowiono to zmienić i faworyzować początkujących i końcowych członków. Brak faworyzowania członków początkowych i końcowych powodował niejasności, których trudno uniknąć.
Johannes Schaub - litb
45

Myślę, że najważniejszą częścią tego pytania jest to, skąd C ++ wie, jaki jest rozmiar tablicy (przynajmniej chciałem to wiedzieć, kiedy znalazłem to pytanie).

C ++ zna rozmiar tablicy, ponieważ jest częścią definicji tablicy - jest to typ zmiennej. Kompilator musi znać typ.

Ponieważ C ++ 11 std::extentmoże służyć do uzyskania rozmiaru tablicy:

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

Oczywiście nie ma to większego sensu, ponieważ musisz wyraźnie podać rozmiar w pierwszym wierszu, który następnie uzyskasz w drugim wierszu. Ale możesz też użyć, decltypea wtedy stanie się bardziej interesujący:

char v[] { 'A', 'B', 'C', 'D' };
int size2{ std::extent< decltype(v) >::value };
std::cout << "Array size: " << size2 << std::endl;
psur
źródło
6
Rzeczywiście, o to pierwotnie pytałem. :)
Paul Manta
19

Zgodnie z najnowszą wersją roboczą C ++ (n3376) instrukcja ranged for jest równoważna z następującą:

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

Dzięki temu wie, jak zatrzymać w taki sam sposób, jak forrobi to zwykła pętla przy użyciu iteratorów.

Myślę, że możesz szukać czegoś podobnego do poniższego, aby zapewnić sposób użycia powyższej składni z tablicami, które składają się tylko ze wskaźnika i rozmiaru (tablice dynamiczne):

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

Ten szablon klasy może być następnie użyty do utworzenia zakresu, po którym można iterować przy użyciu nowego zakresu dla składni. Używam tego do przeglądania wszystkich obiektów animacji w scenie, która jest importowana przy użyciu biblioteki, która zwraca tylko wskaźnik do tablicy i rozmiar jako oddzielne wartości.

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

Moim zdaniem ta składnia jest znacznie jaśniejsza niż to, co można uzyskać za std::for_eachpomocą zwykłej forpętli.

Dotacja
źródło
3

Wie, kiedy przestać, ponieważ zna granice tablic statycznych.

Nie jestem pewien, co masz na myśli przez „tablice dynamiczne”, w każdym razie, jeśli nie iterując po tablicach statycznych, nieformalnie, kompilator wyszukuje nazwy begini endzakres klasy obiektu, po którym iterujesz, lub przegląda się na begin(range)i end(range)za pomocą odnośnika i używa ich jako argumentu iteratory zależne.

Więcej informacji można znaleźć w standardzie C ++ 11 (lub jego publicznej wersji roboczej), „6.5.4 Instrukcja oparta na zakresie for”, str.145

chłód
źródło
4
„Tablica dynamiczna” zostanie utworzona za pomocą new[]. W takim przypadku masz tylko wskaźnik bez wskazania rozmiaru, więc nie ma możliwości, foraby działał na podstawie zakresu .
Mike Seymour
Moja odpowiedź zawiera dynamiczną tablicę, której rozmiar (4) jest znany w czasie kompilacji, ale nie wiem, czy ta interpretacja „tablicy dynamicznej” jest tym, co zamierzał pytający.
Johannes Schaub - litb
3

Jak działa na podstawie zakresu dla zwykłych tablic?

Czy należy to rozumieć jako: „ Powiedz mi, co robi ranged-for (z tablicami)?

Odpowiem zakładając, że - weź następujący przykład z użyciem zagnieżdżonych tablic:

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

for (auto &pl : ia)

Wersja tekstowa:

iajest tablicą tablic („tablica zagnieżdżona”) zawierającą [3]tablice, z których każda zawiera [4]wartości. Powyższy przykład przechodzi iaprzez swój podstawowy „zakres” ( [3]), a zatem wykonuje pętlę [3]razy. Każda pętla wytwarza jeden z ia„S [3]podstawowych wartości, zaczynając od pierwszej a kończąc na ostatnim - tablicę zawierającą [4]wartości.

  • Pierwsza pętla: plrówna się {1,2,3,4}- tablica
  • Druga pętla: plrówna się {5,6,7,8}- tablica
  • Trzecia pętla: plrówna się {9,10,11,12}- tablica

Zanim wyjaśnimy ten proces, oto kilka przyjaznych przypomnień o tablicach:

  • Tablice są interpretowane jako wskaźniki do ich pierwszej wartości - użycie tablicy bez żadnej iteracji zwraca adres pierwszej wartości
  • pl musi być odniesieniem, ponieważ nie możemy kopiować tablic
  • W przypadku tablic, kiedy dodajesz liczbę do samego obiektu tablicy, przesuwa się ona do przodu tyle razy i „wskazuje” na równoważny wpis - Jeśli njest to liczba, to ia[n]jest to samo co *(ia+n)(Wyłuskujemy adres będący nwpisami naprzód) i ia+njest taki sam jak &ia[n](Otrzymujemy adres tego wpisu w tablicy).

Oto co się dzieje:

  • W każdej pętli pljest ustawiana jako odniesienie do ia[n], z nwyrównywaniem bieżącej liczby pętli zaczynając od 0. Tak więc pljest ia[0]w pierwszej rundzie, w drugiej to ia[1]i tak dalej. Pobiera wartość poprzez iterację.
  • Pętla trwa tak długo, jak ia+njest krótsza niż end(ia).

... I to wszystko.

To naprawdę tylko uproszczony sposób pisania tego :

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

Jeśli twoja tablica nie jest zagnieżdżona, ten proces staje się nieco prostszy , ponieważ odwołanie nie jest potrzebne, ponieważ iterowana wartość nie jest tablicą, ale raczej „normalną” wartością:

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

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

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

Dodatkowe informacje

A co by było, gdybyśmy nie chcieli używać autosłowa kluczowego podczas tworzenia pl? Jakby to wyglądało?

W poniższym przykładzie plodwołuje się do pliku array of four integers. Na każdej pętli plpodawana jest wartość ia[n]:

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

I ... Tak to działa, z dodatkowymi informacjami, które pozwolą uniknąć nieporozumień. To tylko „skrócona” forpętla, która automatycznie liczy się za Ciebie, ale brakuje jej sposobu na odzyskanie bieżącej pętli bez robienia tego ręcznie.

Super Cat
źródło
@Andy 9 na 10 razy tytuł jest tym, co pasuje do wyszukiwania w Google / cokolwiek - tytuł pyta, jak to działa? a nie kiedy wie, kiedy przestać? . Mimo to, ukryte pytanie jest w pewnym stopniu ujęte w tej odpowiedzi i przechodzi do odpowiedzi dla każdego, kto szuka innej odpowiedzi. Pytania składniowe, takie jak te, powinny mieć sformułowane tytuły, aby można było napisać odpowiedź, używając tylko tego, ponieważ to wszystkie informacje, których poszukujący potrzebuje, aby znaleźć pytanie. Z pewnością się nie mylisz - pytanie nie jest zatytułowane tak, jak powinno.
Super Cat,
0

Przykładowy kod pokazujący różnicę między tablicami na stosie a tablicami na stercie


/**
 * 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
źródło