Good day to everyone,
I checked the existing proposals regarding array_view (including the multi-dimensional and strided ones) and I found none that would support byte stride (please correct me if I missed one, apologies in advance).
I have my own custom array view written long time ago which I use a lot and found it extremely convenient, and it differs from the proposed
array_view by having an additional runtime parameter
byte_stride (defaulted to
sizeof(T) to have usual
array_view by default).
The
byte_stride is not part of the type, so the type is still the same
array_view<class T, int Rank=1> and you can use it for both contiguous arrays and
byte-strided arrays.
So the constructor looks like this (for 1D array view):
array_view( T* ptr, size_t size, size_t byte_stride = sizeof(T) );
Motivation
In scientific/financial calculations one often has arrays of sets of parameters, i.e. arrays of structs:
struct BasicData {
int data1;
int data2;
};
BasicData barr[50];
It's also not unusual to have extended versions of algorithms that use additional parameters on top of the generic ones (or save additional data on top of the common one).
The natural way of dealing with this is just inheriting from the basic struct and make an array of the derived struct:
struct ExtendedData : BasicData {
int data3;
};
ExtendedData earr[50];
this way you can access common and extended data the same way
without changing your code (and you can freely move parameters from
derived struct to the base and back, without affecting the specialized
code that uses them):
earr.data1[i] = earr.data2[i] * earr.data3[i];
The problem here is that now even the code that doesn't need extended data (like ones gathering common statistics or statuses) and would happily run on just basic data (and therefore could have been factored out to the common unspecialized codebase)
void common_processing(array_view<BasicData>);
Such a function can't run because there is no array of basic params anymore, they are hidden inside the bigger extended params struct, and there is currently no way to get an
array_view<BasicData> out of
earr.
Strided array_view won't help because sizeof(BasicData) is 2*sizeof(int), while sizeof(ExtendedData) is 3*sizeof(int) - so strides of sizeof(BasicData) won't work.
Also, there can be a plugin-like architecture of the program so the host wouldn't even know the type ExtendedData that exists only in the plugin but still could access BasicData owned by the plugin if there was a way to construct
array_view<BasicData>.
All the above is easily achievable with a byte-strided array_view.
Another example: think of BasicData as RGB and ExtendedData as RGBA/ARGB: you could have an array_view<RGB> over an RGBA file and an RGB file and even an ARGB file, agnostically - they will only differ in their byte_stride value.
Here are the use cases (1D, for simplicity):
1) Use as a normal array_view:
struct B {
int x,y;
};
B *pb = new B[50];
array_view<B> av( pb, 50 ); // byte_stride is defaulted to sizeof(B)
2) view of an array of a derived class D as an array of base class B:struct B {
int x,y;
};
struct D: B {
int a;
};
D *pd = new D[50];
array_view<B> av_b( pd, 50, sizeof(D) );
(pd automatically converted to B*, sizeof(B) == 2*sizeof(int), sizeof(D) == 3*sizeof(int) so usual integer stride won't work here because sizeof(D) is not a multiple of sizeof(B))to avoid mistakes, an obvious helper function array_view_by_base() is provided (it also checks static_assert(std::is_base_of_v<B,D>)):
array_view<B> av_b = array_view_by_base<B>( pd, 50 );
3) Member view ("array" of field b):struct B {
int x,y;
};
struct C {
int a;
B b;
};
C *pc = new C[50];
array_view<B> av_b( &pc[0].b, 50, sizeof(C) );
array_view<int> av_y( &pc[0].b.y, 50, sizeof(C) );
Again, to avoid mistakes, a helper function array_view_by_member() is provided:
array_view<B> av_b = array_view_by_member( pc, &C::b, 50 ); // both D and B are deduced from the arguments
Unfortunately, there is currently no way in C++ to have a nested pointer-to-member so it's impossible to write something like
&C::b::y. This can be emulated, to some extent, by providing a tuple like (&C::b, &B::y) but it's not much more readable than the unsafe version. So the safe version for av_y would be:
array_view<int> av_y = array_view_by_member( pc, std::make_tuple(&C::b, &B::y), 50 ); // both D and int are deduced from the arguments
4) Strided views for all the cases above:
Just call the constructor with stride*sizeof(D) instead of just sizeof(D)
Note 1: when the argument p above is not a plain pointer but a container that knows its size (via std::size - think of std::vector, std::array, or even another array_view) then you don't need to provide the size manually:
std::vector<B> vb;
array_view<B> av_b( vb );
std::vector<D> vd;
array_view<B> av_b = array_view_by_base<B>( vd );
std::vector<C> vc;
array_view<B> av_b = array_view_by_member( vc, &C::b );
Note 2: advantage over some other proposals is in the uniform type array_view<T,Rank> that doesn't depend on the "continuousness" (unlike n4177 where strided view is a separate type strided_array_view
).
For those who need to know if the view is contiguous (for example, to do a bulk memcpy instead of element-wise copying) there is a member function bool continuous() const provided that just checks if sizeof(T)==byte_stride.
Note 3: array_view supports default construction and assignment (this is necessary for the cases when we have a view in the host program, then we load a plugin, attach to its memory, and initialized our array_view to look at the plugin's memory, and then we can unload the plugin and load another, with a different derived type, and reinitialized our array_view again).
Comments?
If there is an agreement that it would be good to have (from my experience, it is) I'll write a formal proposal.
Thanks everyone,
Yours sincerely,
Maxim Yanchenko