17.13 — Wielowymiarowe std::array

W poprzedniej lekcji (17.12 — Wielowymiarowe tablice w stylu C), omawialiśmy wielowymiarowe tablice w stylu C.

    // C-style 2d array
    int arr[3][4] { 
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }};

Ale jak wiesz, generalnie chcemy tego unikać Tablice w stylu C (chyba że są używane do przechowywania danych globalnych).

W tej lekcji przyjrzymy się, jak działają tablice wielowymiarowe z std::array.

Nie ma standardowej biblioteki tablic wielowymiarowych. Klasa

Zauważ to std::array jest implementowana jako tablica jednowymiarowa. Zatem pierwszym pytaniem, które powinieneś zadać, jest: „Czy istnieje standardowa klasa biblioteczna dla tablic wielowymiarowych?” Odpowiedź brzmi… nie. Szkoda. Womp womp.

Dwuwymiarowy std::array

Kanoniczny sposób utworzenia dwuwymiarowej tablicy std::array polega na utworzeniu std::array gdzie argumentem typu szablonu jest inny std::array. Prowadzi to do czegoś takiego:

    std::array<std::array<int, 4>, 3> arr {{  // note double braces
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }}};

Jest wiele „interesujących” rzeczy, na które warto zwrócić uwagę:

  • Podczas inicjowania wielowymiarowego std::array musimy użyć podwójnych nawiasów klamrowych (omówimy, dlaczego w lekcji 17.4 -- std::array typów klas i eliminacji nawiasów klamrowych).
  • Składnia jest szczegółowa i trudne do odczytania.
  • Ze względu na sposób zagnieżdżania szablonów wymiary tablicy są zamieniane. Chcemy tablicy z 3 rzędami po 4 elementy, więc arr[3][4] jest to naturalne. std::array<std::array<int, 4>, 3> jest odwrotne.

Indeksowanie dwuwymiarowego std::array elementu działa podobnie jak indeksowanie dwuwymiarowego elementu w stylu C tablica:

    std::cout << arr[1][2]; // print the element in row 1, column 2

Możemy także przekazać funkcję dwuwymiarową std::array do funkcji tak samo jak jednowymiarową std::array:

#include <array>
#include <iostream>

template <typename T, std::size_t Row, std::size_t Col>
void printArray(const std::array<std::array<T, Col>, Row> &arr)
{
    for (const auto& arow: arr)   // get each array row
    {
        for (const auto& e: arow) // get each element of the row
            std::cout << e << ' ';

        std::cout << '\n';
    }
}

int main()
{
    std::array<std::array<int, 4>, 3>  arr {{
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }}};

    printArray(arr);

    return 0;
}

Fuj. I to jest w przypadku dwuwymiarowej std::array. Trójwymiarowa lub wyższa std::array jest jeszcze bardziej gadatliwa!

Ułatwianie tworzenia dwuwymiarowych std::array za pomocą szablonów aliasów

W lekcji 10.7 -- Typedefs i aliasy typów, wprowadziliśmy aliasy typów i zauważyliśmy, że jednym z zastosowań aliasów typów jest uproszczenie użycia typów złożonych. Jednakże w przypadku aliasów typu normalnego musimy jawnie określić wszystkie argumenty szablonu, np.

using Array2dint34 = std::array<std::array<int, 4>, 3>;

Pozwala nam to używać Array2dint34 wszędzie tam, gdzie chcemy dwuwymiarowej std::array 3×4. of int. Ale pamiętaj, że potrzebujemy jednego takiego aliasu dla każdej kombinacji typu elementu i wymiarów, których chcemy użyć!

To idealne miejsce na użycie szablonu aliasu, który pozwoli nam określić typ elementu, długość wiersza i długość kolumny dla aliasu typu jako argumenty szablonu!

// An alias template for a two-dimensional std::array
template <typename T, std::size_t Row, std::size_t Col>
using Array2d = std::array<std::array<T, Col>, Row>;

Możemy wtedy użyć Array2d<int, 3, 4> w dowolnym miejscu dwuwymiarowego 3×4 std::array of int. Jest o wiele lepiej!

Oto pełny przykład:

#include <array>
#include <iostream>

// An alias template for a two-dimensional std::array
template <typename T, std::size_t Row, std::size_t Col>
using Array2d = std::array<std::array<T, Col>, Row>;

// When using Array2d as a function parameter, we need to respecify the template parameters
template <typename T, std::size_t Row, std::size_t Col>
void printArray(const Array2d<T, Row, Col> &arr)
{
    for (const auto& arow: arr)   // get each array row
    {
        for (const auto& e: arow) // get each element of the row
            std::cout << e << ' ';

        std::cout << '\n';
    }
}

int main()
{
    // Define a two-dimensional array of int with 3 rows and 4 columns
    Array2d<int, 3, 4> arr {{
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }}};

    printArray(arr);

    return 0;
}

Zauważ, że jest to o wiele bardziej zwięzłe i łatwe w użyciu!

Jedną z zalet naszego szablonu aliasów jest to, że możemy zdefiniować parametry szablonu w dowolnej kolejności. Ponieważ std::array określa najpierw typ elementu, a następnie wymiar, trzymamy się tego zgodnie z tą konwencją, ale możemy zdefiniować najpierw Row lub Col Ponieważ definicje tablic w stylu C są definiowane w pierwszym wierszu, definiujemy nasz szablon aliasu za pomocą Row przed Col.

Ta metoda również ładnie skaluje się do wyższych wymiarów std::array:

// An alias template for a three-dimensional std::array
template <typename T, std::size_t Row, std::size_t Col, std::size_t Depth>
using Array3d = std::array<std::array<std::array<T, Depth>, Col>, Row>;

Uzyskiwanie długości wymiarowych tablicy dwuwymiarowej

W przypadku jednowymiarowej std::array możemy użyć size() funkcję składową (lub std::size()), aby uzyskać długość tablicy. Ale co robimy, gdy mamy dwuwymiarową std::array? W tym przypadku size() zwróci tylko długość pierwszego wymiaru.

Jedną z pozornie atrakcyjnych (ale potencjalnie niebezpiecznych) opcji jest uzyskanie elementu o żądanym wymiarze, a następnie wywołaj size() na tym elemencie:

#include <array>
#include <iostream>

// An alias template for a two-dimensional std::array
template <typename T, std::size_t Row, std::size_t Col>
using Array2d = std::array<std::array<T, Col>, Row>;

int main()
{
    // Define a two-dimensional array of int with 3 rows and 4 columns
    Array2d<int, 3, 4> arr {{
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }}};

    std::cout << "Rows: " << arr.size() << '\n';    // get length of first dimension (rows)
    std::cout << "Cols: " << arr[0].size() << '\n'; // get length of second dimension (cols), undefined behavior if length of first dimension is zero!

    return 0;
}

Aby uzyskać długość pierwszego wymiaru, wywołujemy size() na tablicy. Aby uzyskać długość drugiego wymiaru, najpierw wywołujemy arr[0] , aby uzyskać pierwszy element, a następnie wywołujemy size() , aby uzyskać długość trzeciego wymiaru tablicy 3-wymiarowej, wywołujemy arr[0][0].size().

Powyższy kod ma jednak wady, ponieważ będzie generował niezdefiniowane zachowanie, jeśli dowolny wymiar inny niż ostatni ma długość 0!

Lepszą opcją jest użycie szablonu funkcji do zwrócenia długości wymiaru bezpośrednio z powiązanego parametru szablonu innego niż typ:

#include <array>
#include <iostream>

// An alias template for a two-dimensional std::array
template <typename T, std::size_t Row, std::size_t Col>
using Array2d = std::array<std::array<T, Col>, Row>;

// Fetch the number of rows from the Row non-type template parameter
template <typename T, std::size_t Row, std::size_t Col>
constexpr int rowLength(const Array2d<T, Row, Col>&) // you can return std::size_t if you prefer
{
    return Row;
}

// Fetch the number of cols from the Col non-type template parameter
template <typename T, std::size_t Row, std::size_t Col>
constexpr int colLength(const Array2d<T, Row, Col>&) // you can return std::size_t if you prefer
{
    return Col;
}

int main()
{
    // Define a two-dimensional array of int with 3 rows and 4 columns
    Array2d<int, 3, 4> arr {{
        { 1, 2, 3, 4 },
        { 5, 6, 7, 8 },
        { 9, 10, 11, 12 }}};

    std::cout << "Rows: " << rowLength(arr) << '\n'; // get length of first dimension (rows)
    std::cout << "Cols: " << colLength(arr) << '\n'; // get length of second dimension (cols)

    return 0;
}

Unika to niezdefiniowanego zachowania, jeśli długość dowolnego wymiaru wynosi zero, ponieważ używa tylko informacji o typie tablicy, a nie rzeczywistych danych tablicy. Pozwala nam to również łatwo zwrócić długość jako int jeśli chcemy (nie jest wymagane żadne rzutowanie statyczne, ponieważ konwersja z constexpr std::size_t Do constexpr int nie zawęża, więc konwersja niejawna jest w porządku).

Spłaszczanie dwuwymiarowej tablicy

Tablice z dwoma lub większą liczbą wymiarów wiążą się z pewnymi wyzwaniami:

  • Są bardziej szczegółowe w definiowaniu i działaniu with.
  • Uzyskiwanie długości wymiarów większych niż pierwszy jest niewygodne.
  • Iterowanie po nich jest coraz trudniejsze (wymaga jednej pętli więcej dla każdego wymiaru).

Jednym ze sposobów ułatwienia pracy z tablicami wielowymiarowymi jest ich spłaszczenie. Spłaszczanie tablica to proces zmniejszania wymiarowości tablicy (często do jednego wymiaru).

Na przykład zamiast tworzyć dwuwymiarową tablicę z Row wierszami i Col kolumny, możemy stworzyć jednowymiarową tablicę zawierającą Row * Col elementy. Daje nam to taką samą ilość pamięci przy użyciu jednego wymiaru.

Jednakże, ponieważ nasza jednowymiarowa tablica ma tylko jeden wymiar, nie możemy pracować z nią jako tablicą wielowymiarową. Aby rozwiązać ten problem, możemy zapewnić interfejs imitujący tablicę wielowymiarową. Interfejs ten akceptuje współrzędne dwuwymiarowe, a następnie mapuje je do unikalnej pozycji w tablicy jednowymiarowej.

Oto przykład takiego podejścia, które działa w C++11 lub nowszym:

#include <array>
#include <iostream>
#include <functional>

// An alias template to allow us to define a one-dimensional std::array using two dimensions
template <typename T, std::size_t Row, std::size_t Col>
using ArrayFlat2d = std::array<T, Row * Col>;

// A modifiable view that allows us to work with an ArrayFlat2d using two dimensions
// This is a view, so the ArrayFlat2d being viewed must stay in scope
template <typename T, std::size_t Row, std::size_t Col>
class ArrayView2d
{
private:
    // You might be tempted to make m_arr a reference to an ArrayFlat2d,
    // but this makes the view non-copy-assignable since references can't be reseated.
    // Using std::reference_wrapper gives us reference semantics and copy assignability.
    std::reference_wrapper<ArrayFlat2d<T, Row, Col>> m_arr {};

public:
    ArrayView2d(ArrayFlat2d<T, Row, Col> &arr)
        : m_arr { arr }
    {}

    // Get element via single subscript (using operator[])
    T& operator[](int i) { return m_arr.get()[static_cast<std::size_t>(i)]; }
    const T& operator[](int i) const { return m_arr.get()[static_cast<std::size_t>(i)]; }

    // Get element via 2d subscript (using operator(), since operator[] doesn't support multiple dimensions prior to C++23)
    T& operator()(int row, int col) { return m_arr.get()[static_cast<std::size_t>(row * cols() + col)]; }
    const T& operator()(int row, int col) const { return m_arr.get()[static_cast<std::size_t>(row * cols() + col)]; }

    // in C++23, you can uncomment these since multidimensional operator[] is supported
//    T& operator[](int row, int col) { return m_arr.get()[static_cast<std::size_t>(row * cols() + col)]; }
//    const T& operator[](int row, int col) const { return m_arr.get()[static_cast<std::size_t>(row * cols() + col)]; }

    int rows() const { return static_cast<int>(Row); }
    int cols() const { return static_cast<int>(Col); }
    int length() const { return static_cast<int>(Row * Col); }
};

int main()
{
    // Define a one-dimensional std::array of int (with 3 rows and 4 columns)
    ArrayFlat2d<int, 3, 4> arr {
        1, 2, 3, 4,
        5, 6, 7, 8,
        9, 10, 11, 12 };

    // Define a two-dimensional view into our one-dimensional array
    ArrayView2d<int, 3, 4> arrView { arr };

    // print array dimensions
    std::cout << "Rows: " << arrView.rows() << '\n';
    std::cout << "Cols: " << arrView.cols() << '\n';

    // print array using a single dimension
    for (int i=0; i < arrView.length(); ++i)
        std::cout << arrView[i] << ' ';

    std::cout << '\n';

    // print array using two dimensions
    for (int row=0; row < arrView.rows(); ++row)
    {
        for (int col=0; col < arrView.cols(); ++col)
            std::cout << arrView(row, col) << ' ';
        std::cout << '\n';
    }

    std::cout << '\n';

    return 0;
}

Wypisuje:

Rows: 3
Cols: 4
1 2 3 4 5 6 7 8 9 10 11 12
1 2 3 4
5 6 7 8
9 10 11 12

Ponieważ operator[] może akceptować tylko jeden indeks dolny przed C++23, istnieją dwa alternatywne podejścia:

  • Użyj operator() zamiast tego, które może akceptować wiele indeksów dolnych. Dzięki temu można używać [] do indeksowania pojedynczego indeksu i () do indeksowania wielu wymiarów. Zdecydowaliśmy się na to podejście powyżej.
  • Zwróć operator[] widok podrzędny, który również przeciąża operator[] , dzięki czemu możesz połączyć operator[]. Jest to bardziej złożone i nie skaluje się dobrze do wyższych wymiarów.

W C++23 operator[] został rozszerzony, aby akceptować wiele indeksów dolnych, więc można go przeciążyć, aby obsługiwał zarówno pojedynczy, jak i wiele indeksów dolnych (zamiast używać operator() dla wielu indeksów dolnych).

Powiązana treść

Omawiamy std::reference_wrapper w lekcji 17.5 -- Tablice odniesień poprzez std::reference_wrapper.

std::mdspan C++23

Wprowadzony w C++ 23, std::mdspan to modyfikowalny widok zapewniający wielowymiarowy interfejs tablicowy dla ciągłej sekwencji elementów. Przez modyfikowalny widok rozumiemy, że std::mdspan nie jest tylko widokiem tylko do odczytu (jak std::string_view) — jeśli leżąca u jego podstaw sekwencja elementów nie jest stała, elementy te można modyfikować.

Poniższy przykład wyświetla te same dane wyjściowe, co poprzedni przykład, ale używa std::mdspan zamiast naszego własnego niestandardowego widoku:

#include <array>
#include <iostream>
#include <mdspan>

// An alias template to allow us to define a one-dimensional std::array using two dimensions
template <typename T, std::size_t Row, std::size_t Col>
using ArrayFlat2d = std::array<T, Row * Col>;

int main()
{
    // Define a one-dimensional std::array of int (with 3 rows and 4 columns)
    ArrayFlat2d<int, 3, 4> arr {
        1, 2, 3, 4,
        5, 6, 7, 8,
        9, 10, 11, 12 };

    // Define a two-dimensional span into our one-dimensional array
    // We must pass std::mdspan a pointer to the sequence of elements
    // which we can do via the data() member function of std::array or std::vector
    std::mdspan mdView { arr.data(), 3, 4 };

    // print array dimensions
    // std::mdspan calls these extents
    std::size_t rows { mdView.extents().extent(0) };
    std::size_t cols { mdView.extents().extent(1) };
    std::cout << "Rows: " << rows << '\n';
    std::cout << "Cols: " << cols << '\n';

    // print array in 1d
    // The data_handle() member gives us a pointer to the sequence of elements
    // which we can then index
    for (std::size_t i=0; i < mdView.size(); ++i)
        std::cout << mdView.data_handle()[i] << ' ';
    std::cout << '\n';

    // print array in 2d
    // We use multidimensional [] to access elements
    for (std::size_t row=0; row < rows; ++row)
    {
        for (std::size_t col=0; col < cols; ++col)
            std::cout << mdView[row, col] << ' ';
        std::cout << '\n';
    }
    std::cout << '\n';

    return 0;
}

Powinno to być dość proste, ale są kilka rzeczy wartych odnotowania:

  • std::mdspan zdefiniujmy widok z dowolną liczbą wymiarów.
  • Pierwszy parametr konstruktora std::mdspan powinien być wskaźnikiem do danych tablicy. Może to być tablica w stylu C z rozkładem lub możemy użyć data() funkcją składową std::array lub std::vector aby uzyskać te dane.
  • Aby zaindeksować std::mdspan w jednym wymiarze musimy pobrać wskaźnik do danych tablicy, co możemy zrobić za pomocą funkcji składowej data_handle() . Możemy to następnie zapisać w indeksie dolnym.
  • W C++23 operator[] akceptuje wiele indeksów, więc używamy [row, col] jako nasz indeks zamiast [row][col].

C++26 będzie zawierał std::mdarray, co zasadniczo łączy std::array i std::mdspan w posiadającą wielowymiarową tablicę!

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
62 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze