Nastoletni
Programiści

Logo Nastoletnich Programistów

Kontenery w C++ – std::array, czyli ładniejsza tablica.

W standardowych i popularnych kursach języka C++ zazwyczaj nikt nie rusza tematów poświęconych szczegółom takim jak kontenery, czy też inne pierdoły z referencji języka – mogą one się wydać rozpraszające lub po prostu trudne dla początkującego, lub komuś po prostu się nie chce. Dlatego postanowiłem napisać serię poradników poświęconych właśnie takim szczegółom, które potrafią bardzo ułatwić życie a o których rzadko kiedy się pisze. Zakładam oczywiście, że czytelnik posiada podstawową wiedzę na temat C++’a, bo bez tego może być mu ciężko ugryźć ten poradnik.

Zacznę od podstawowego kontenera, jakim jest std::array.

Zakładam, że wiesz co to tablica, prawda? Ot, zbiór elementów jednego typu poukładanych w pamięci jeden za drugim. Nic strasznego, nic trudnego. Jest to bardzo prosta rzecz, można by rzec nawet, że aż za prosta – standardowy C-style array nie posiada w sobie nic ciekawego, żadnych iteratorów, żadnych fajnych metod dostępu do niego, wszystko trzeba robić „ręcznie”.

I tutaj na scenie pojawia się std::array z biblioteki array, dodany w standardzie C++11

Ważne – (zazwyczaj) domyślnie każdy kompilator korzysta ze standardu C++03. Żeby skorzystać z nowszych, należy to sprecyzować przy użyciu flagi -std=c++11/14/17 (zależnie od standardu), albo w opcjach kompilatora – ale to zostawiam już do samodzielnego ogarnięcia).

Jest to bardzo przyjemna alternatywa, posiadająca dużą wartość użytkową – po prostu ułatwia programiście życie. No dobra, ale co w tym takiego fajnego? Dlaczego miałbym się przestawić nagle na std::array, zamiast używać starego dobrego c-style arraya?

But… why?

Używanie std::array jest prawie takie samo jak używanie zwykłego c-style arraya – losowy dostęp do elementów umożliwia standardowy operator [], ale oprócz tego ten kontener posiada kilka bajerów:

Po pierwsze – std::array posiada tak zwany iterator. Iterator to obiekt wskazujący na jakiś element z zakresu elementów, który potrafi iterować (czyli, potocznie mówiąc, „przeskakiwać”) między elementami przy pomocy na przykład operatorów inkrementacji czy dekrementacji (ale częściej iteratorów używa się ich w range-based for’ach na przykład, o czym za chwilę). Jest to duże ułatwienie w sytuacjach gdzie musimy na przykład przelecieć całą tablicę od początku do końca (lub też od końca do początku, bo std::array posiada także reverse iterator!)

Po drugie – posiada przeładowane operatory porównania ==, !=, <, <=, >, >=. Czyli możemy w łatwy sposób porównywać dwa std::array’e. Dodatkowo, ma przeładowania dla std::swap i std::get, gdybyśmy potrzebowali w fikuśny sposób dostać się do danego elementu tablicy lub zamienić jej zawartość z zawartością innej.

Po trzecie – posiada kilka użytkowych metod, takich jak fill (wypełnienie tablicy pewną wartością),  front/back (dostęp do pierwszego/ostatniego elementu), empty (sprawdzenie czy tablica jest pusta), size/max_size (sprawdzenie aktualnej i maksymalnej ilości elementów). Jest to też typ agregowalny, czyli przykładowo można go inicjalizować poprzez klamry {}, tak jak zwykłą tablicę.

Podsumowując – std::array jest prawie taki sam w użytkowaniu jak zwykły c-style array, tylko fajniejszy oraz często wygodniejszy.

No fajnie, tylko jak tego użyć?

Jak już wcześniej mówiłem, można używać std::array tak samo jak zwykłej tablicy. Inaczej wygląda deklaracja, ponieważ jest to zwyczajna struktura z szablonowymi typami w konstruktorze:

template<class T, std::size_t N> struct array;

Gdzie T to typ danych przechowywanych w naszym kontenerze, a N to jego początkowa wielkość. std::size_t to ogólnie mówiąc typ danych dla liczb całkowitych bez znaku.

Warto o tym pamiętać, polecam przy odnoszeniu się do wielkości kontenerów używać std::size_t zamiast int’a czy jakiegokolwiek innego typu danych, oszczędzi nam to ostrzeżeń kompilatora mówiących o porównywaniu typów ze znakiem i bez niego.

Przykładowe deklaracje takich kontenerów wyglądają następująco

No i fajnie, mamy jakieśtam tablice. Co teraz z nimi zrobić? Wykorzystajmy dla przykładu iteratory.

Ten kod chyba wymaga tłumaczenia, szczególnie jeśli ktoś wcześniej nie miał kontaktu z nowszymi standardami C++.

Więc tak – pierwszy przykład wyświetla nam zawartość std::array przy użyciu range-based for’a. const auto &v to stała referencja do kolejnych wartości kontenera, iterator automatycznie inkrementuje się co iterację pętli. Dlaczego odnoszę się do elementów w ten sposób? Mógłbym to zrobić klasycznie, poprzez int v, ale w ten sposób każdy z elementów byłby kopiowany do tymczasowej zmiennej v, co w przypadku większych kontenerów stwarzało by duży problem wydajnościowy. Odniesienie się poprzez referencję pozwala na kopiowanie jedynie adresu do elementu, unikając kopiowania jego samego, a const uniemożliwia zmianę danego elementu (bo nie chcemy go przecież zmieniać). W tym wypadku jest to na tyle mała skala że w praktyce nie zauważymy żadnych wzrostów wydajności, ale warto wyrobić sobie praktykę używania referencji. Jeśli chodzi o auto, to jest to tylko kwestia wygody – ten keyword i tak zostanie zamieniony przez kompilator na odpowiedni typ danych.

W następnym kawałku kodu wypełniamy sobie tablicę bezpośrednio odnosząc się do iteratorów – b.begin() wskazuje na początek, b.end() na koniec naszego kontenera (analogicznie, b.rbegin() wskazuje na koniec, a b.rend() na początek). Jak wcześniej mówiłem, iteratory mają przeładowane operatory – inkrementacja, dekrementacja i tego typu rzeczy, które są bardzo wygodne.

Dalej, modyfikacja elementów przy użyciu range-based for’a, tutaj używam referencji bez const ponieważ modyfikuję elementy (i w tym wypadku nie mogę odnieść się bez referencji, ponieważ bez niej operował bym na kopiach elementów tablicy)

I na koniec smaczek, przelatuję sobie tablicę od końca co drugi element.

Ale jak by to wyglądało na normalnej tablicy?

Oczywiście to tylko kilka przykładowych porównań, ale widzimy, że std::array jest o wiele bardziej czytelniejszy niż standardowa tablica.

Podsumowanie – plusy i minusy

Przyszedł czas na krótkie podsumowanie naszego kontenera

  • + Czytelny – posiada wiele funkcji które pozwalają na pisanie czytelnego kodu z wykorzystaniem naszego arraya
  • + Wygodny – Iteratory, przeładowania operatorów, metody pozwalające na sprawdzenie wielkości i dostęp do krańcowych elementów oraz wiele więcej metod i bajerów ułatwiających życie
  • + Kompatybilny – Można go używać w ten sam sposób, jak normalnej tablicy. Ale nie trzeba.
  • – Stała wielkość – Jeśli potrzebujemy kontenera o zmiennej wielkości, należy użyć na przykład std::vector

I w sumie tyle. Jeśli masz jakieś wątpliwości, nadal czegoś nie rozumiesz, zapomniałem o czymś, masz jakąś sugestię czy cokolwiek – pisz śmiało w komentarzu.

A po pełną referencję i więcej przykładów dla tego kontenera zapraszam tutaj: http://en.cppreference.com/w/cpp/container/array.

Następny rozdział będzie poświęcony kontenerowi std::vector.

Wojciech Olech

Samouk, teleinformatyk, programista. Bawię się elektronicznym stuffem. Ulubione platformy: Arduino, AVR, RaspberryPi, Linux, Windows. Ulubione języki: C/C++/C#/Python.

Zobacz wszystkie posty tego autora →

Komentarze

  • Desant.

    for (std::size_t i = 0; i < size_cA; ++i)
    std::cout << cA[i] << " ";
    std::cout << std::endl;

    Dlaczego nie tak samo jak z std::array?

    • Wojciech Olech

      można tak samo z std::array, ale można też użyć iteratorów.
      Zależy od sytuacji.

  • Bielan

    1. Dlaczego porównujesz dynamiczną tablicę do std::array, które nie posiada takiej możliwości? To powinno wylecieć z porównania.
    2. Absolutny błąd odnośnie range based fora oraz iteratorów dla zwykłej tablicy
    int tab[] = {3, 4, 6, 8, 9};

    for(const auto & elem : tab)
    {
    std::cout << elem;
    }

    auto beg = std::begin(tab);
    auto rbeg = std::rbegin(tab);
    auto end = std::end(tab);
    auto rend = std::rend(tab);

    Przecież to działa!

    3. Brak informacji o std:array:fill, który można realizować za pomocą std::fill na zwykłej tablicy.
    4. Z całego artykułu zostaje informacja o przeładowanych operatorach natomiast cała reszta wprowadza w błąd! Czy ktoś to w ogóle redagował?

    5. std::aray posiada wiele zalet, niestety artykuł o tym nie wspomina.

    • Wojciech Olech

      Pisząc ten artykuł byłem jeszcze niespełna rozumu na temat, więc teraz mogę to odkopać i się odnieść:
      1. Faktycznie, porównywanie vectora do arraya w tym kontekście nie jest poprawne
      2. Faktycznie, nie wiedziałem o tym, ciekawa sprawa – myślałem że range-based for wymaga iteratorów w kontenerze
      3. Też fakt, ale skupiłem się na tym co oferuje array, to nie artykuł stricte o algorytmach z STLa
      4. Nie jestem pewien o jaką „resztę” która wprowadza w błąd ci chodzi, możesz wskazać? (tak, zdaje sobie sprawę że to ma 8 miesięcy, ale fizycznie nie widzę tutaj momentów w których wprowadzam w błąd)
      5. Żeby opisać wszystkie zalety arraya, artykuł musiał by być znaaacznie dłuższy, dlatego skupiłem się na podstawach

  • Wojciech Olech

    To ja tak skomentuję, z racji że w tej chwili nie do końca mam jak edytować ten post – Pisałem ten post *bardzo dawno temu*, na dodatek na szybko.
    Z tego względu nie przeczę że merytorycznie jest on kulawy, oraz wymaga porządnego zredagowania go.
    Kiedy znajdę chwilę, postaram się go poprawić i przepisać od nowa.