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::arraymusimy 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 2Moż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ążaoperator[], 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::mdspanzdefiniujmy widok z dowolną liczbą wymiarów.- Pierwszy parametr konstruktora
std::mdspanpowinien 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::arraylubstd::vectoraby uzyskać te dane. - Aby zaindeksować
std::mdspanw jednym wymiarze musimy pobrać wskaźnik do danych tablicy, co możemy zrobić za pomocą funkcji składowejdata_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ę!

