Introduction
Why use fixed size arrays?
Why bother with plain arrays when we have std::array?
How we missed passing plain arrays by reference
Emulating class method semantics for plain arrays
Names
Using the code
Array methods reference
Insecure cast reference
Summary
History
Presented here is an emulation of class methods that can be used directly on naked plain arrays as you find them in your code.
#include "array_methods.h"
using namespace array_methods;
int arr[10][10];
int res = arr->*at(5)->*at(3);
You may well ask why bother now that we have std::array
. Here are two reasons:
- Existing code already uses plain arrays and there is a lot of it.
- Plain arrays remain the only safe choice for multi dimensional arrays that are mathematically navigable with a single direct offset. This is discussed in more detail further below.
The 'methods' provided are modelled on those of std::array
but there are some differences apart from being invoked by the ->*
operator rather than the .
operator, particularly with regard to multidimensional arrays.
Here they are:
The ->*'methods'
array->*get_size() array->*get_rank() array->*get_volume()
array[index] also supports functional style dereferencing e.g. 5[4[3[array]]].
array->*at(index) array->*at<false>(index) array->*at<index>() array->*7_th
The element access methods also support functional style dereferencing.
e.g. 5_th[4_th[at(x)[array]]].
array->*copy(src_array) array->*fill(value or array) array->*swap(src_array)
array->*get_begin() array->*get_cbegin()
array->*get_end() array->*get_cend()
array->*get_rbegin() array->*get_rcbegin()
array->*get_rend() array->*get_rcend()
array->*get_begin_volume() array->*get_cbegin_volume()
array->*get_end_volume() array->*get_cend_volume()
array->*get_rbegin_volume() array->*get_rcbegin_volume()
array->*get_rend_volume() array->*get_rcend_volume()
The imperative for emulating class method semantics came from the need to dereference multi dimensional arrays comfortably
int a[10][10][10];
int i1 = array->*at(x)->*at(y)->*at(z);
int i2 = array->*5_th->*6_th->*7_th;
It also sits well with other operations, putting the array first and then what should be done with it.
array1->*copy(array2);
Interopabilty between plain array and std::array
All of these ->*
'methods' except the volume iterators will also work with std::array
and, should you construct or come across them, they will also work with any multidimensional composite of plain arrays and std::array
s e.g.
std::array<int[10][10], 10> arr;
That is to say they are agnostic to whether an array is plain array, std::array
or a combination of both. This is also the case with the arguments to the copy
, fill
and swap
'methods'. As a result they will operate seamlessly with whatever mixtures and messes of plain array and std::array
that you find in your code, bridging the unhelpful forking of the canonical representation of fixed size arrays that std::array
has introduced.
Accommodation of std::vector
As std::array
is designed so that it will work with code written for std::vector
, it makes sense that the ->*
'methods' that unify plain arrays and std::array
should also work with std::vector
. Accordingly all ->*
'methods' except the volume iterators and fill
will also work with std::vector
. However with std::vector
there are some restrictions:
- A
std::vector
may copy an array or std::array
but neither an array or std::array
may copy a std::vector
- Only a
std::vector
can swap with a std::vector
.
Could we not just cast our plain arrays to std::array
to use its interface? Yes you can and no you shouldn't. It is a hack that one day might bite you hard. Nevertheless casting is a hack that can be useful for bridging gaps during prototyping so some fail safe multidimensional reference casting functions are provided. They are discussed separately in the insecure cast reference section.
First we need to talk about why we should give so much importance to the handling of plain arrays when the Standard Library's collection classes now include std::array
. Or even why use fixed size arrays when we have std::vector
.
First there is what you get just for not requiring it to be a dynamic array. A dynamic array such as std::vector
requires a heap allocation and subsequent de-allocation whereas a fixed size array, be it plain array or std::array
, is allocated on the stack which is much faster and keeps its contents close to where the action is.
Secondly there is what you get from the compiler because of the assumptions a fixed size array allows it to make. I don't know the half of this but it can assume that elements of a fixed size array live as long as the array itself and do not move around in memory during that time and that probably facilitates quite a few optimisations.
Then there are the assumptions that it allows you to make. You too know that a reference to an element of a fixed size array will remain valid as long as the array exists and this can mean continuing to work with an existing reference instead of having to return to the array to dereference it again as you might have to with a dynamic array such as std::vector
.
Sometimes there is just no requirement for an array to be dynamic so why pay for the extra overhead. Sometimes fixing the size is a worthwhile trade off for the optimisation opportunities that it provides.
- The fact that most code already uses plain arrays is a formidable obstacle to eradicating them from the language.
- They have always been part of the language and are therefore more standard than the Standard Library.
- Their absence of class wrapper guarantees the integrity of the fully specified contiguous memory layout of multidimensional arrays, a feature exploited by much of the code that uses them.
- They are the language's own built in template type and this can be fully exploited by generic code if you know how to pass them by reference.
I need say no more about the first of these and the second is no more than a plea for respect. It is the last two that are not so obvious, require explanation, and are the key to what can and can't be done. The first of those is discussed here.
Lets look at a 2 dimensional plain array
int array[4][3];
conceptually its layout is
0:0 0:1 0:2
1:0 1:1 1:2
2:0 2:1 2:2
3:0 3:1 3:2
and its physical memory layout is
0:0 0:1 0:2 1:0 1:1 1:2 2:0 2:1 2:2 3:0 3:1 3:2
which is guaranteed to be one contiguous block of data with no fillers, markers or padding.
Knowing this, instead of dereferencing them with array[x][y]
, you can deference them with *(array + 3*x + y)
. This may seem perverse at first sight but it is a single mathematical expression yielding one value that gives you a direct reference into the array. To dereference it formally you always have calculate, carry and produce two indices to access an element of data.
A lot of existing code exploits this and it can have irresistible and unmatched mathematical advantages in the design of new code. In many cases it is the reason for accepting the constraints of fixed size arrays. It is common in mathematical and statistical analysis and these single offsets into a multidimensional volume can be produced by complex and sophisticated algorithms that may not even go anywhere near multiple indices as a frame of reference. Here is a simple one that doesn't:
int array[4][3];
int* p = &array[0][0];
int* end = p + 4*3;
for(p; p != end; p++)
*p = 5;
The array is filled with 5
's without any reference to the first and second index at all.
Now the big question is can you do this with multidimensional std:array
s?
std::array<std::array<int, 3>, 4> arr;
The answer is tricky. As far as anyone knows a multidimensional std:array
will produce the same memory layout as the equivalent plain array but the standard does not require it to. That is not same solid guarantee that you get with plain arrays and you have to wonder why the standard stops short of requiring it.
The std::array
standard requires that its elements be held as a plain array and that it should be the first data member and the language standard requires that the first data member sits right at the beginning of the class. Implementations of std::array
are free to add data members after the array elements and compilers are free to pad the end of a class. If either were to happen then a multi-dimensional std:array
will not be one unbroken block of data. It will be broken up by the data or padding at the end of each constituent array:
0:0 0:1 0:2 ### 1:0 1:1 1:2 ### 2:0 2:1 2:2 ### 3:0 3:1 3:2 ###
and that is going to completely mess up algorithmic single reference access based on knowledge of the array dimensions.
The vexing thing is that as I far as I know this never happens but the standard does not guarantee it. Everything will be OK until a debug version of std::array
gets decorated by an extra data member or a compiler setting pads the end of your std::array
class wrappers.
The outcome is that you cannot safely base your code on the assumption that a multi-dimensional std:array
has the same memory layout as an equivalent plain array even though it usually does. To do so is a hack and even if your code never breaks it will invalidate your insurance and no language lawyer will be able to help you. If you don't think this is anything to worry about then you can happily accept the hack of casting a plain array to std::array
mentioned in the introduction. Both are hostages to the same memory layout insecurity and its politics.
Plain arrays don't suffer this insecurity because they have no wrapper class in which to contain any extra data members or padding. They are baked directly into the language and the layout rules are part of the language specification. Compilers can't be asked to make a special accommodation for std::array
but they do obey language rules.
This doesn't mean that you shouldn't use multi-dimensional std::array
s. std::array
is a thin and transparant wrapper around a plain array and sometimes that can be very useful. You can't have a std::vector
of plain arrays but you can have a vector of std::array
s - just having a class wrapper does that. They are fine as long as you use them just as you would a multi-dimensional std::vector
and don't expose them to code that makes assumptions about how they are laid out in memory.
Of course those assumptions are very empowering, sometimes they are the big upside to fixing the array sizes, and that is when you should use plain arrays rather than std::array
. Even if you don't do that sort of thing, expect to continue seeing multidimensional plain arrays created by those that do.
Most importantly, if you go on a plain array cleansing exercise and redeclare existing multidimensional plain arrays as std::array
s then you may be putting the code that uses them in jeopardy.
I hope this helps anyone who is being pressurised by 'quality initiatives' to turn
int arr[10][15][20];
into
std::array<std::array<std::array<int, 20>, 15>, 10> arr;
The syntax reflects the memory layout situation:
The first indicates the component arrays butting directly against each other which reflects the guaranteed contiguous multidimensional memory layout that will be produced
The second arouses a suspicion that this might be being compromised and indeed it is.
On the issue of passing arrays into functions, almost all tutorials say
“You can't pass an array by value because they are not copyable so instead you pass a pointer to its first element”
and in doing so they mislead us with a false binary choice. We swallow it so easily because the next thing they tell us is
“You just have to pass the name of the array and it will automatically decay to a pointer to the first element”.
As a result the following practice has been canonised:
void my_func(int* pA) {
}
int arr[10];
my_func(arr);
and the following rarely considered and the syntax even more rarely achieved
void my_func1(int(&a)[10]) {
}
int arr[10];
my_func(arr);
It is the key to more intelligent handling of plain arrays. First you have a fully typed array to work with rather than just a pointer to an element and also the same technique allows you write generic functions that can interrogate your fully typed array and carry out type aware operations:
template<class T, size_t N>
inline constexpr size_t get_size(T(&a)[N])
{
return N;
}
int arr[10];
size_t size = get_size(a);
including bounds checked access functions
template<class T, size_t N>
inline T& size_t get_at(T(&a)[N], size_t index)
{
if ( index < N)
return a[index];
throw std::out_of_range("array index");
}
template<size_t Index, class T, size_t N>
typename std::enable_if<(N > Index), T&>::type
get_at(T(&a)[N])
{
return a[I];
}
int arr[10];
int i = get_at(arr, 7);
int i = get_at<7>(arr);
With this it becomes apparent that plain arrays are not as poor as we might have thought. They are the language's own built in template type which you could conceptualise as
template<class T, size_t N> T [ N ]
They are just as type rich as a std::array<T, N>
and knowing how to pass them by reference unlocks the power of this. Here is that syntax again
T(&array_name)[N]
The bounds checked access function described above
int i1 = get_at(arr, index);
is only slightly uglier than the class method call you get with std::array
int i1 = arr.at(index);
However with multidimensional arrays, the nested double parameter calls to get()
start to become uncomfortable to form and interpret correctly
int aaa[10][10][10];
int res1 = aaa[3][4][5];
int res2 = get_at(get_at(get_at(aaa, 4), 5), 3);
This is a simple semantic issue that function calls require all operands to be enclosed in brackets following the function name. However operators don't. A global binary operator will take the first operand from before its symbol and the second following it and doesn't require any enclosure with brackets.
The normal use of the ->*
operator is for dereferencing a pointer to member function which plain arrays can't have and it is the tightest binding (highest precedence) binary operator that can be overloaded. So we will use that.
I knew that a global operator overload must have at least one user defined type involved in its arguments but for a moment I still thought this might work:
template<class T, size_t N>
T operator ->* (T(&array)[N], size_t index);
but no, the compiler doesn't buy the idea that T(&array)[N]
is a user defined type because it isn't. What we can do though is wrap our index in a user defined type
struct at_index
{
size_t index;
at_index(size_t _index) : index(_index) {}
};
We can now define an operator that takes any array and our new user defined type at_index
and carries out a run-time bounds check:
template<class T, size_t N>
T& operator ->* (T(&a)[N], at_index i)
{
if (i.index < N)
return a[i.index];
throw std::out_of_range("array index");
}
finally we need a function that returns a suitably initialised at_index
object with the name we want to use as a 'method' call.
inline auto at(size_t index)
{
return at_index(index);
}
and now we can write:
int a[10];
int res = a->*at(7);
and
int aa[10][10];
int res = aa->*at(7)->*at(5);
This is the same as you would write with std::array
but using ->*
instead of the .
operator. However the ->*
operator does not have the same precedence as the .
operator or []
operator. So if your expression requires further dereferencing of the result then you will need to bracket your ->*
invoked array accesses.
widget widgets[10][10];
gromet& g = ( widgets->*at(7)->*at(5) ).grmt;
Omitting those brackets will not cause any misinterpretation. It will refuse to compile.
For our compile-time bounds checked access with constant indices we define another wrapper class which has the index bound into its type which enables it to be passed as a template parameter. It holds no data:
template <size_t I>
struct at_const
{
enum { value = I };
constexpr at_const() {}
};
and we define another overload of the ->*
operator to work with it
template<size_t I, class T, size_t N>
constexpr typename std::enable_if<(N > I), T&>::type
operator ->* (T(&a)[N], at_const<I>)
{
return a[I];
}
and a different overload of the function at
that we will use as the 'method' call.
template <size_t I>
inline constexpr auto at()
{
return at_const_index<I>();
}
So we can write:
int a[10];
int res = a->*at<7>();
and
int aa[10][10];
int res = a->*at<7>()->*at<5>();
but at<7>()
is a bit ugly so we define a user defined literal for cases where the index is a literal constant.
template<char ... Args>
constexpr auto operator "" _th()
{
return at_const<int_from_char_args<Args...>()>();
}
and now we can write:
int a[10];
int res = a->*7_th;
and with a multidimensional array:
int a[10][10];
int res = a->*7_th->*5_th;
However you will still have to use the uglier form if your index is a symbolic constant rather than a literal constant:
constexpr size_t index = 5;
int res = a->*at<index>();
To gain a more direct insight into how it works let us decompose a call to the at(index)
'method':
array->*at(6);
at(6)
merely returns an at_index
object holding the value of 6
. The operation is executed by the operator ->*
overload that matches a right hand type of at_index
. It puts together the two parameters (array from its LHS) and index (extracted from the at_index
on its RHS) and executes the operation. The 'methods' simply return tokens indicating the operation to be carried out and carrying the RHS argument if there is one. The method call and construction of the at_index
object are elided during compilation leaving only the operator ->*
overload to be executed at run-time. This leaves the run-time overhead the same as that of a normal method call.
I originally intended to publish these 'findings' as a Tip/Trick and leave it at that but I enjoy coding more than writing and found myself carrying out the due diligence to turn it into a library supporting the methods you might expect plus specific support for multidimensional arrays.
One of the good things about real class methods is that their names are scoped by the type of the object they operate on. This means there is no risk of them colliding with the same name used elsewhere. As a result std::array
can have a method called size()
even though size
is a popular name for a local variable. There is no problem writing:
size_t size = array.size();
The plain array 'methods' provided here act semantically as a method would, but they are really free functions in disguise, and unlike real methods, their names could potentially clash with other classes, functions or variables or even namespace names.
The library encloses these 'methods' inside namespace array_methods
. A name which is descriptive and hopefully unique. The user is then left to choose to:
Promote all of the methods to the global namespace:
using namespace array_methods;
int arr[10][10][10];
arr->*fill(0);
arr->*at(4)->*at(5)->*at(6) = 23;
only promote selected methods to the global namespace:
using array_methods::at;
using array_methods::operator""_th;
int arr[10][10][10];
arr->*array_methods::fill(0); arr->*at(4)->*at(5)->*at(6) = 23;
or don't promote any and qualify them all with array_methods::
each time you use them which is not so comfortable.
int arr[10][10][10];
arr->*array_methods::fill(0);
arr->*array_methods::at(4)->*array_methods::at(5)->*array_methods::at(6) = 23;
If you go down this route then you may find it convenient to alias array_methods
to something shorter and easier to type:
namespace arr_mtds = array_methods;
int arr[10][10][10];
arr->*arr_mtds::fill(0);
arr->arr_mtds::at(4)->*arr_mtds::at(5)-*>arr_mtds::at(6) = 23;
The 'methods' and their names have been designed so that promoting all methods to the global namespace
using namespace array_methods;
has a very good chance of working out well to give you maximum convenience with no name collisions:
at
is the most vulnerable having only two letters, yet it still has a very good chance of standing uncontested. at
is an adverb and therefore not a sensible choice for a class or variable and there is no problem with other functions called at
as long as they don't have the same 'deficient' argument list. Probably the greatest threat is another library with a root namespace called at
. There is a politics of root namespace names that has yet to be thrashed out. std
and boost
have uncontested pedigree but even they have chosen names that you would not otherwise want to use in your code. operator “” _th
is a user defined literal and it has to be promoted to the global namespace if you want to use it because you can't qualify an operator as you use it. The most likely other use of user defined literals is in units of measurement libraries (e.g. 5_metres
) - I released one myself recently . I chose _th
because it generically represents sequence and I couldn't think of any unit of measurement for which it might be suited. copy
, fill
and swap
are already functions in the standard library. Although you are not supposed to write using std;
that is still enough to deter using copy
or fill
as class names or variables. Lots of people write functions called copy
and fill
but none of them will have the same 'deficient' argument lists. - The remaining 'methods' are all prefixed with
get_
to turn them into verbs that you would not use as class or variable names. Other functions with the same name are no problem as long as they don't have the same 'deficient' argument lists.
For this reason I recommend promoting all methods with
using namespace array_methods;
and enjoy the ease of use.
If you do encounter name clashes then you will have to promote the methods more selectively or not at all. Retrospective global edits to conform with this will not be difficult to carry out because these 'methods' are always anchored to the ->*
operator.
It is written for C++ 11 and requires <stdexcept> and <type_traits> to be in the include path as they usually will be with a C++11 instalation.
You need to downdload array_methods.zip and extract array_methods.h
Then include the following before your use of ->*
'methods'
#include "array_methods.h"
using namespace array_methods;
The previous section explains how namespace array_methods
is designed to avoid name collisions and the measures that can be taken should they occur.
With this done you can simply apply these ->*
'methods' to any plain arrays and also std:array
s and std::vector
s that you find in your code.
There is something else though. These ->*
'methods' work on arrays and references to arrays, so you might want to start writing some of your functions to take arrays by reference rather than a pointer to its first element. This will bring the array into your function where you can work with it as an array, including the application of ->*
'methods'.
void my_func1(int(&arr)[10][10]) {
arr->*fill(4);
for (auto& ar : arr)
for (auto& e : ar)
e *= 2;
int res1 = arr->*4_th->*5_th;
int res2 = arr->*at(res1)->*at(res1);
auto iter = arr->*get_begin();
auto end = arr->*get_end();
for (iter; iter != end; iter++)
(*iter)->*4_th *= 3;
auto v_iter = arr->*get_begin_volume();
auto v_end = arr->*get_end_volume();
for (v_iter; v_iter != v_end; v_iter++)
*v_iter *= 3; }
The ->*
'methods' available are listed and described in the reference section that follows.
The insecure hack of casting between plain arrays and std::array
is discussed separately in the insecure cast reference section below.
//_______________array properties______________________
array->*get_size() //number of elements in first rank
array->*get_rank() //number of dimensions
array->*get_volume() //total number of elements in multidimensional array
//________________element access______________________
array[index] //not a ->*
method but we should not forget it
also supports functional style dereferencing e.g. 5[4[3[array]]]
.
array->*at(index) // run-time bounds check
array->*at<false>(index) // no bounds check
array->*at<index>() // compile-time bounds check with symbolic constant
array->*7_th // compile-time bounds check with literal constant
The element access methods also support functional style dereferencing.
e.g. 5_th[4_th[at(x)[array]]]
.
//______________copy, fill and swap_____________________
array->*copy(src_array) //copies all elements of src_array to itself
array->*fill(value or array) //fills array or volume
array->*swap(value or array) //swaps contents with another array
//___________________ iterators________________________
array->*get_begin() //get iterator set to beginning of the array
array->*get_cbegin() //iterator cannot be used to mutate the array
array->*get_end() //get iterator set to one past the end of the array
array->*get_cend() //iterator cannot be used to mutate the array
/_________________ reverse iterators_______ _____________
array->*get_rbegin() //get reverse iterator begin = end of the array
array->*get_rcbegin() //iterator cannot be used to mutate the array
array->*get_rend() //get reverse iterator end = last address before beginning of array
array->*get_rcend() //iterator cannot be used to mutate the array
//_______________volume iterators_______________________
array->*get_begin_volume() //get iterator set to beginning of volume
array->*get_cbegin_volume() //iterator cannot be used to mutate the array
array->*get_end_volume() //get iterator set to one past the end of the volume
array->*get_cend_volume() ///iterator cannot be used to mutate the array
//____________ reverse volume iterators___________________
array->*get_rbegin_volume() //get reverse volume iterator begin = end of the array
array->*get_rcbegin_volume() ///iterator cannot be used to mutate the array
array->*get_rend_volume() //get reverse volume iterator end = volume begin -1
array->*get_rcend_volume() ///iterator cannot be used to mutate the array
Either
using namespace array_methods;
or (comment out the ones you don't want to promote)
using array_methods::get_rank;
using array_methods::get_size;
using array_methods::get_volume;
using array_methods::at;
using array_methods::operator""_th;
using array_methods::copy;
using array_methods::fill;
using array_methods::swap;
using array_methods::get_begin;
using array_methods::get_cbegin;
using array_methods::get_end;
using array_methods::get_cend;
using array_methods::get_begin_volume;
using array_methods::get_cbegin_volume;
using array_methods::get_end_volume;
using array_methods::get_cend_volume;
array->*get_size() | number of elements in first rank |
Operates on: | plain array, std::array or std::vector . |
Returns: | number of elements in first rank as size_t . |
This is does the same as the .size()
method of std::array
. It returns the number elements in the array. In the case of multidimensional arrays it will be the number of elements in the first rank. e.g
int a[10];
size_t n1 = a->*get_size();
int aaa[10][10][10];
size_t n2 = aaa->*get_size();
array>*get_rank() | number of dimensions |
Operates on: | plain array, std::array or std::vector or any multidimensional hybrid of them. |
Returns: | The number of dimensions in a multi-dimensional array as size_t . |
int a[10];
size_t n1 = a->*get_rank();
int aaa[10][10][10];
size_t n2 = aaa->*get_rank();
array>*get_volume() | total number of stored data elements in multidimensional volume. |
Operates on: | plain array, std::array or std::vector or any multidimensional hybrid of them. |
Returns: | The total number of stored elements in a multi-dimensional array as size_t . |
int a[10];
size_t n1 = a->*get_volume();
int aaa[10][10][10];
size_t n2 = aaa->*get_volume();
Note: For fixed sized arrays get_volume()
is a simple calculation that can be carried out during compilation. However if std::vector
s are part of the volume, it is an extensive recursive iteration that has to be carried out at run-time.
array[index] | built in element access of plain arrays and operator overload of std::array and std::vector |
Operates on: | plain array, std::array or std::vector |
Takes: | size_t index which may be a variable. |
Returns: | A reference to the selected element or a const reference if the array is const. |
Also supports functional style dereferencing: index[array]
is equivalent to array[index]
Although not one of the ->*
'methods' provided here, this remains the default for element access that requires no bounds checks.
size_t i;
for(i= 0; i != array->*get_size(); i++)
array[i] = 6;
array->*at(index) | element access with run-time bounds check. |
Operates on: | plain array, std::array or std::vector |
Takes: | A size_t index which may be a variable. |
Returns: | A reference to the selected element or a const reference if the array is const. |
Throws: | run-time exception if the index is out of bounds. |
Also supports functional style dereferencing: at(index)[array]
is equivalent to array->*at(index)
This was the initial 'holy grail'. An encapsulation of automatic bounds checked access for plain arrays that follows class method semantics to provide the familiar and intuitive sequential indexing into multi-dimensional arrays
array->*at(x)->*at(y)->*at(z)
array->*at<false>(index) | no bounds check, same as array[index] |
Operates on: | plain array, std::array or std::vector |
Takes: | A size_t index which may be a variable. |
Returns: | A reference to the selected element or a const reference if the array is const. |
Also supports functional style dereferencing: at<false>(index)[array]
is equivalent to array->*at<false>(index)
The same array->*as at(index)
but with no bounds checking. Which is to say exactly the same as array[index]
. It is there to facilitate retospective or reversible choices to skip bounds checking. So you can write:
array->*at(x)->*at<false>(y)->*at(z)
instead of
(array->*at(x))[y]->*at(z)
array>*at<INDEX>() | compile-time bounds check with symbolic constant |
Operates on: | plain array, std::array or std::vector |
Takes: | A size_t index as a template parameter which must be a compile time constant. |
Returns: | A reference to the selected element or a const reference if the array is const. |
Fails to compile: | if the index is out of bounds or index is not a compile -time constant. |
Also supports functional style dereferencing: at<INDEX>()[array]
is equivalent to array->*at<INDEX>()
If the index is a constant known during compilation then we can go a step further and do the bounds checking during compilation. That avoids the embarrassment of being surprised by a run-time exception and it also means the dereference can proceed without further bounds checking at run-time.
constexpr size_t INDEX = 7;
array->*at<INDEX>()
For std::vector
this will decay to a run-time bounds check.
array->*7_th | compile-time bounds check with literal constant |
Operates on: | plain array, std::array or std::vector |
Takes: | A literal constant index bound to suffix _th . |
Returns: | A reference to the selected element or a const reference if the array is const. |
Fails to compile: | If the index is out of bounds or index is not a compile -time constant. |
Also supports functional style dereferencing; 7_th[array]
is equivalent to array->*7_th
This is a shorthand for array->*at<INDEX>()
that can be used when the index is a literal constant.
array->*4_th->*5_th->*6_th
For std::vector
this will decay to a run-time bounds check.
array->*copy(src_array) | copies all elements of src_array to itself |
Operates on: | A plain array or std::array or std::vector or any multidimensional hybrid of them. |
Takes: | A plain array or std::array or std::vector or any multidimensional combination with the same dimensional configuration and stored data that is copyable to its own stored type. |
Returns: | void |
There are some restrictions with std::vector
:
a std::vector
may copy an array or std::array
but niether an array or std::array
may copy a std::vector.
The ->*copy
method provides an explicit copy method which will also copy between std::array
and plain arrays and std::vector
or any multidimensional combination of them - subject to the restrictions on std::vector
described above.
Example usage:
int a[10][15];
int b[10][15];
a->*copy(b);
std::array<std::array<int, 15>, 10> sa;
sa->*copy(a);
std::array<int[10], 10> sxa;
sxa->*copy(sa);
array->*swap(other_array) | swaps its contents with other_array |
Operates on: | plain array, std::array or std::vector or any multidimensional hybrid of them. |
Takes: | A plain array, std::array or std::vector or any multidimensional hybrid of them with the same dimensional configuration and stored data that is mutually swappable with its own stored type. |
Returns: | void |
Like the ->*copy
method it is indifferent to whether the arrays are expressed as plain arrays, std::array
s, std::vector
s or a multidimensional hybrid. However any std::vector
s must occupy the same rank in both arguments because only a std::vector
can swap with a std::vector
.
array->*fill(value or array) | fills array or volume |
Operates on: | plain array or std::array or any multidimensional hybrid of them |
Takes: | Any value, array, std::array or multidimensional hybrid of them with which it can be filled |
Returns: | void |
A std::vector
cannot be filled with ->*fill
, neither can it be used as the argument to fill anything else.
The ->*fill
'method' can fill a multi-dimensional array in more than one way
int arr[3][3];
arr->*fill(5);
producing
5, 5, 5
5, 5, 5
5, 5, 5
arr->*fill({1, 2, 3})
producing
1, 2, 3
1, 2, 3
1, 2, 3
It will fill with any value or array that matches the elements of any of its constituent arrays.
For example an int[10][10][10]
can be be filled by:
an int[10][10]
which is an element of int[10][10][10]
an int[10]
which is an element of int[10][10]
or an int
which is an element of int[10]
Like the ->*copy
and ->*
swap 'methods' it is indifferent to whether the arrays are expressed as plain arrays, std::arrays or a multidimensional hybrid.
array->*get_begin() | get iterator set to beginning of first rank |
Operates on: | plain array, std::array or std::vector . |
Returns: | Iterator (pointer to element type) set to beginning of first rank array or an iterator to const if the array is const |
array->*get_cbegin()
| same as get_begin() but iterator cannot be used to mutate the array
|
get_begin()
simply returns a pointer to the beginning of the array which the Standard Library will accept as a valid iterator.
array->*get_end() | get iterator set to one past the end of the first rank |
Operates on: | plain array, std::array or std::vector . |
Returns: | Iterator (pointer to element type) set to one past the end of the first rank array or an iterator to const if the array is const |
array->*get_cend()
| same as get_end() but iterator cannot be used to mutate the array
|
Note the iterator returned by get_end()
is set to the first address beyond the end of array. It does not itself point into the array and dereferencing it will produce undefined behaviour.
Typical iterator usage:
auto iter = arr->*get_begin();
auto end = arr->*get_end();
for (iter; iter != end; ++iter)
*(iter) += 5;
array->*get_rbegin() | get reverse iterator set to its begin which is really the end of the array |
Operates on: | plain array, std::array or std::vector . |
Returns: | Reverse iterator set to its begin which is really the end of the array or a reverse iterator to const if the array is const |
array->*get_rcbegin()
| same as get_rbegin() but iterator cannot be used to mutate the array
|
Reverse iterators are a sleight of hand where: begin means end, end means begin, ++
means - -
and even dereference is skewed so begin is valid (dereferencing what is really the last element of the array) and end is invalid for dereference (pointing at just before the beginning of the array).
array->*get_rend() | get reverse iterator set to its end which is really the last address before the beginning of the array |
Operates on: | plain array, std::array or std::vector . |
Returns: | Reverse iterator set to its end which is really the last address before the beginning of the array or a reverse iterator to const if the array is const |
array->*get_rcend()
| same as get_rend() but iterator cannot be used to mutate the array
|
Note the iterator returned by get_rend()
is set to the last address before the beginning of the array. It does not itself point into the array and dereferencing it will produce undefined behaviour.
Typical reverse iterator usage:
auto iter = arr->*get_rbegin();
auto end = arr->*get_rend();
for (iter; iter != end; ++iter)
*(iter) += 5;
Note that the algorithm is that of a forward iteration. It becomes a reverse iteration simply by passing it reverse iterators.
Volume iteration is not available on std::array
because it depends on the guaranteed standard memory layout of multidimensional plain arrays nor on std::vector
which does not create contigous multidimensional data.
array->*get_begin_volume() | get iterator set to beginning of volume |
Operates on: | plain array with any number of dimensions. |
Returns: | Iterator (pointer to stored data type) set to beginning of volume or an iterator to const if the array is const |
array->*get_cbegin_volume()
| same as get_begin_volume() but iterator cannot be used to mutate the array
|
get_begin_volume()
returns a pointer to the first stored data item in a multidimensional volume. It is used to iterate through every element of stored data in a multidimensional volume
array->*get_end_volume() | get iterator set to one past the end of the volume |
Operates on: | plain array with any number of dimensions. |
Returns: | Iterator (pointer to stored data type) set to one past the end of the volume or an iterator to const if the array is const |
array->*get_cend_volume()
| same as get_end_volume() but iterator cannot be used to mutate the array
|
Note the iterator returned by get_end_volume()
is set to the first address beyond the end of the volume. It does not itself point into the array and dereferencing it will produce undefined behaviour.
Typical volume iterator usage:
int arr[10][10];
auto iter = arr->*get_begin_volume();
auto end = arr->*get_end_volume();
for (iter; iter != end; ++iter)
*(iter) += 5;
Reverse volume iteration is not available on std::array because it depends on the guaranteed standard memory layout of multidimensional plain arrays nor on std::vector
which does not create contigous multidimensional data.
array->*get_rbegin_volume() | get reverse iterator set to its begin which is really the end of the volume |
Operates on: | plain array with any number of dimensions. |
Returns: | Reverse iterator set to its begin which is really the end of the volume or a reverse iterator to const if the array is const |
array->*get_rcbegin_volume()
| same as get_rbegin_volume() but iterator cannot be used to mutate the array
|
Reverse iterators are a sleight of hand where: begin means end, end means begin, ++
means - -
and even dereference is skewed so begin is valid (dereferencing what is really the last stored element of the volume) and end is invalid for dereference (pointing at just before the beginning of the volume.
array->*get_rend_volume() | get reverse iterator set to its end which is really the last address before the beginning of the volume |
Operates on: | plain array with any number of dimensions. |
Returns: | Reverse iterator set to its end which is really the last address before the beginning of the volume or a reverse iterator to const if the array is const |
array->*get_rcend_volume()
| same as get_rend_volume() but iterator cannot be used to mutate the array
|
Note the iterator returned by get_rend_volume()
is set to the last address before the beginning of the volume. It does not itself point into the array and dereferencing it will produce undefined behaviour.
Typical reverse volume iterator usage:
int arr[10][10];
auto iter = arr->*get_rbegin_volume();
auto end = arr->*get_rend_volume();
for (iter; iter != end; ++iter)
*(iter) += 5;
Note that the algorithm is that of a forward iteration. It becomes a reverse iteration simply by passing it reverse iterators.
If you are going to cast from a plain array to a std::array
or vice versa, the following functions will do the cast for you in an organised and fail safe manner. That is to say, if it isn't going to work out then they won't compile. You should know though that if they compile (as they very likely will) it only means that it is OK for your current std::array
implementation, compiler and settings. That is OK for prototyping and also for getting an emergency release out to keep a client running but it is not a good foundation for building your code. A change in the design of std::array
or a new compiler optimisation that pads your std::array
could leave you unable to compile your code.
array_insecure_cast::as_std_array(array) | casts to a std::array |
Operates on: | plain array or std::array or any multidimensional hybrid of them. |
Returns: | A reference to a dimensionally equivalent std::array |
Fails to compile: | If the result would not have the same memory layout as the argument with the current std::array imlementation, compiler and settings. |
You may have a system that has been written to work with std::array
and calls its methods extensively and you want to try it with a new data set that comes as plain arrays. The ->*
'methods' aren't going help you here because the code has already been written using proper .
methods. array_insecure_cast::as_std_array
will allow you to try the new data set before making huge changes to your code.
auto& std_array = array_insecure_cast::as_std_array(plain_array);
If the new data set turns out to work out ok then you now have more work to do because you shouldn't leave it like that and the array_insecure_cast::
qualifier is there to remind you of that. Don't write using array_insecure_cast
in your code. You'll be digging a hole. Let the words array_insecure_cast
appear everywhere that you use it. The unease that those words invoke is entirely appropriate and should be felt every time these casts are seen, even though they seem to always work.
array_insecure_cast::as_plain_array(array) | casts to a plain array |
Operates on: | plain array or std::array or any multidimensional hybrid of them.. |
Returns: | A reference to a dimensionally equivalent plain_array |
Fails to compile: | If the result would not have the same memory layout as the argument with the current std::array imlementation, compiler and settings. |
You may want to feed a multidimensional std::array
to code that works with plain arrays. Again, if it compiles it will work safely but the day may come when it doesn't compile.
The initial imperative for this project was to provide convenient bounds checked access to the elements of plain arrays e.g. ->*at(index)
. As many plain arrays are multi-dimensional, it also made sense to have methods that acknowledge and support this including a ->*fill
method that does what want you want and expect in a multidimensional volume. This remains where I most expect it to be used and that can be anecdotally and as required. It makes no demand that it should be used systematically.
Beyond that, it also provides a unified coding expression for plain arrays, std::array
s and std::vector
s and its implementation of ->*copy
, ->*swap
and ->*fill
provide an environment in which they can meet and exchange data safely and comfortably. Because of this there are benefits in using them systematically should you be bold enough to make that choice.
I hope these ->*
'methods' will be helpful however they are used.
First release: 04 Jan 2018.