In this example, we show how to construct and work with views and slices.
All the following code snippets are part of the same main
function:
#include <iostream>
int main(int argc, char *argv[]) {
}
Includes all relevant headers for the core nda library.
Creating a full view on an array/view
We have already seen in Working with views how we can get a full view on an existing array by doing an empty function call:
for (int i = 0; auto &x : A) x = i++;
auto A_v = A();
std::cout << "A_v = " << A_v << std::endl;
basic_array< ValueType, Rank, Layout, 'A', ContainerPolicy > array
Alias template of an nda::basic_array with an 'A' algebra.
Output:
A_v =
[[0,1,2,3,4]
[5,6,7,8,9]
[10,11,12,13,14]
[15,16,17,18,19]
[20,21,22,23,24]]
The same could have been achieved by
Since views behave mostly as arrays, we can also take a view of a view:
auto A_vv = A_v();
std::cout << "A_vv = " << A_vv << std::endl;
Output:
A_vv =
[[0,1,2,3,4]
[5,6,7,8,9]
[10,11,12,13,14]
[15,16,17,18,19]
[20,21,22,23,24]]
Value type of views
While the value type of an array is always non-const, views can have const or non-const value types.
Usually, views will have the same value type as the underlying array/view:
static_assert(std::is_same_v<decltype(A_v)::value_type, int>);
A_v(0, 0) = -1;
std::cout << "A = " << A << std::endl;
Output:
A =
[[-1,1,2,3,4]
[5,6,7,8,9]
[10,11,12,13,14]
[15,16,17,18,19]
[20,21,22,23,24]]
This is not the case if we take a view of a const array/view, then the value type will be const:
static_assert(std::is_same_v<decltype(A_vc)::value_type, const int>);
auto A_vvc = A_vc();
static_assert(std::is_same_v<decltype(A_vvc)::value_type, const int>);
A generic multi-dimensional array.
As expected, we cannot assign to const arrays/views or views with a const value type, i.e.
Creating a slice of an array/view
Working with slices has already explained what a slice is and how we can create one. In the following, we will give some more examples to show how slices can be used in practice.
Here is the original array that we will be working on:
A(0, 0) = 0;
std::cout << "A = " << A << std::endl;
Output:
A =
[[0,1,2,3,4]
[5,6,7,8,9]
[10,11,12,13,14]
[15,16,17,18,19]
[20,21,22,23,24]]
Let us start by taking a slice of every other column:
auto S_1 = A(nda::range::all, nda::range(0, 5, 2));
std::cout << "S_1 = " << S_1 << std::endl;
Output:
S_1 =
[[0,2,4]
[5,7,9]
[10,12,14]
[15,17,19]
[20,22,24]]
Now we take a slice of every other row of this slice:
auto S_2 = S_1(nda::range(0, 5, 2), nda::range::all);
std::cout << "S_2 = " << S_2 << std::endl;
Output:
S_2 =
[[0,2,4]
[10,12,14]
[20,22,24]]
We could have gotten the same slice with
auto S_3 = A(nda::range(0, 5, 2), nda::range(0, 5, 2));
std::cout << "S_3 = " << S_3 << std::endl;
Output:
S_3 =
[[0,2,4]
[10,12,14]
[20,22,24]]
Assigning to views
Before assigning to the view S_3
, let's make a copy of its contents so that we can restore everything later on:
decltype(auto) make_regular(A &&a)
Make a given object regular.
nda::make_regular turns view, slices and lazy expressions into regular nda::basic_array objects.
A_tmp
is an nda::array of the same size as S_3
and contains the same values but in a different memory location.
Now, we can assign to the view just like to arrays (see Example 3: Initializing arrays):
S_3 = 0;
std::cout << "S_3 = " << S_3 << std::endl;
Output:
S_3 =
[[0,0,0]
[0,0,0]
[0,0,0]]
Any changes that we make to a view will be reflected in the original array and all other views that are currently looking at the same memory locations:
std::cout << "S_1 = " << S_1 << std::endl;
std::cout << "A = " << A << std::endl;
Output:
S_1 =
[[0,0,0]
[5,7,9]
[0,0,0]
[15,17,19]
[0,0,0]]
A =
[[0,1,0,3,0]
[5,6,7,8,9]
[0,11,0,13,0]
[15,16,17,18,19]
[0,21,0,23,0]]
To restore the original array, we can assign our copy from before:
S_3 = A_tmp;
std::cout << "A = " << A << std::endl;
Output:
A =
[[0,1,2,3,4]
[5,6,7,8,9]
[10,11,12,13,14]
[15,16,17,18,19]
[20,21,22,23,24]]
Note: In contrast to arrays, views cannot be resized. When assigning some general nda::Array object to a view, their shapes have to match, otherwise this may result in undefined behavior.
Copy/Move operations
The copy and move operations of views are a little bit different than their array counterparts:
- The copy constructor makes a new view that points to the same data as the other view.
- The move constructor does the same as the copy constructor.
- The copy assignment operator makes a deep copy of the contents of the other view.
- The move assignment operator does the same as the copy assignment.
auto S_3_copy = S_3;
std::cout << "S_3.data() == S_3_copy.data() = " << (S_3.data() == S_3_copy.data()) << std::endl;
auto S_3_move = std::move(S_3);
std::cout << "S_3.data() == S_3_move.data() = " << (S_3.data() == S_3_move.data()) << std::endl;
auto B_v = B();
B_v = S_3;
std::cout << "B = " << B << std::endl;
auto C_v = C();
C_v = std::move(S_3);
std::cout << "C = " << C << std::endl;
Output:
S_3.data() == S_3_copy.data() = 1
S_3.data() == S_3_move.data() = 1
B =
[[0,2,4]
[10,12,14]
[20,22,24]]
C =
[[0,2,4]
[10,12,14]
[20,22,24]]
Operating on views/slices
We can perform various arithmetic operations, mathematical functions and algorithms with views and slices just like we did with arrays in Performing arithmetic operations and Applying mathematical functions and algorithms.
std::cout << "C = " << C << std::endl;
std::cout <<
"sum(S_3) = " <<
nda::sum(S_3) << std::endl;
auto max_element(A const &a)
Find the maximum element of an array.
auto sum(A const &a)
Sum all the elements of an nda::Array object.
auto min_element(A const &a)
Find the minimum element of an array.
auto pow(A &&a, double p)
Function pow for nda::ArrayOrScalar types (lazy and coefficient-wise for nda::Array types).
Output:
C =
[[0,8,24]
[120,168,224]
[440,528,624]]
min_element(S_3) = 0
max_element(S_3) = 24
sum(S_3) = 108
product(S_3) = 0
Rebinding a view to another array/view
If we want to bind an existing view to a new array/view/memory location, we cannot simply use the copy assignment (since it makes a deep copy of the view's contents). Instead we have to call nda::basic_array_view::rebind:
std::cout << "S_3.data() == C_v.data() = " << (S_3.data() == C_v.data()) << std::endl;
C_v.rebind(S_3);
std::cout << "S_3.data() == C_v.data() = " << (S_3.data() == C_v.data()) << std::endl;
Output:
S_3.data() == C_v.data() = 0
S_3.data() == C_v.data() = 1
Viewing generic 1-dimensional ranges
The views in nda can also view generic 1-dimensional ranges like std::vector
or std::array
. The only requirement is that they are contiguous:
std::array<double, 5> arr{1.0, 2.0, 3.0, 4.0, 5.0};
std::cout << "arr_v = " << arr_v << std::endl;
arr_v *= 2.0;
std::cout << "arr = " << arr << std::endl;
A generic view of a multi-dimensional array.
Output:
arr_v = [1,2,3,4,5]
arr = (2 4 6 8 10)
Factories and transformations
Factories and transformations contain various functions to create new and transform existing views.
In the following, we will show some examples.
Let us start from this array:
for (int i = 0; auto &x : D) x = i++;
std::cout << "D = " << D << std::endl;
Output:
D =
[[0,1,2,3]
[4,5,6,7]
[8,9,10,11]]
We can transpose D
using nda::transpose:
std::cout << "D_t = " << D_t << std::endl;
auto transpose(A &&a)
Transpose the memory layout of an nda::MemoryArray or an nda::expr_call.
Output:
D_t =
[[0,4,8]
[1,5,9]
[2,6,10]
[3,7,11]]
Note: D_t
is a view and not an array. That means that transposing an existing array/view is a very cheap operation since it doesn't allocate or copy any data.
The same is true for most of the other transformations.
As long as the data is contiguous in memory and the memory layout is either in C-order or Fortran-order, we are allowed to change the shape of an array/view:
std::cout << "D_r = " << D_r << std::endl;
std::cout << "D_tr = " << D_tr << std::endl;
auto reshape(A &&a, std::array< Int, R > const &new_shape)
Reshape an nda::basic_array or nda::basic_array_view.
Output:
D_r =
[[0,1,2,3,4,5]
[6,7,8,9,10,11]]
D_tr =
[[0,2,4,6,8,10]
[1,3,5,7,9,11]]
To interpret some array/view as a contiguous 1-dimensional range, we can call nda::flatten which is just a convenient wrapper around nda::reshape:
std::cout << "D_t_flat = " << D_t_flat << std::endl;
std::cout << "D_flat = " << D_flat << std::endl;
auto flatten(A &&a)
Flatten an nda::basic_array or nda::basic_array_view to a 1-dimensional array/view by reshaping it.
Output:
D_t_flat = [0,1,2,3,4,5,6,7,8,9,10,11]
D_flat = [0,1,2,3,4,5,6,7,8,9,10,11]
nda provides some more advanced transformations which are especially useful for higher-dimensional arrays/views. We refere the interested user to the API Documentation.