Click here to Skip to main content
15,884,032 members
Articles / Programming Languages / C++11

'Class methods' for plain arrays and std::array and vector too.

Rate me:
Please Sign up or sign in to vote.
4.78/5 (9 votes)
4 Jan 2018CPOL31 min read 9.1K   88   7  
Class method emulation for plain arrays plus unified handling of plain arrays, std::arrays and std::vectors

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

Introduction

Presented here is an emulation of class methods that can be used directly on naked plain arrays as you find them in your code. 

C++
#include "array_methods.h"
using namespace array_methods;

int arr[10][10];  //a plain array not wrapped, preprepared or registered in any way

int res = arr->*at(5)->*at(3); //bounds checked element access

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'
C++
//_______________________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] //the native access operator of a plain array
     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, same as array[index]
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(src_array) //swaps contents with another array

//_______________________iterators_______________________________
array->*get_begin()    //get iterator set to beginning of array
array->*get_cbegin()    //iterator cannot be used to mutate the array

array->*get_end()        //get iterator set to one past the end of 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 volume

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 volume

//__________________reverse volume iterators_________________________________
array->*get_rbegin_volume()   //get reverse volume iterator begin = end of the volume
array->*get_rcbegin_volume()  //iterator cannot be used to mutate the volume

array->*get_rend_volume()     //get reverse volume iterator end = volume begin -1
array->*get_rcend_volume()    //iterator cannot be used to mutate the volume

The imperative for emulating class method semantics came from the need to dereference multi dimensional arrays comfortably

C++
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.

C++
array1->*copy(array2); //there is clarity that array1 is copying array2 and not vice versa
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::arrays e.g.

C++
std::array<int[10][10], 10> arr; //a std_array of 10 int[10][10]s

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:

  • 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.
Casting a plain array to std::array to use its methods?

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.

Why use fixed size arrays?

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.

Why bother with plain arrays when we have std::array?

  • 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

C++
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:

C++
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:arrays?

C++
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::arrays.  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::arrays -  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::arrays 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

C++
int arr[10][15][20];

into

C++
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.

How we missed passing plain arrays by reference 

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:

C++
void my_func(int* pA) // a pointer to an int
{
    //do things with pA, an int*
    
}

int arr[10];
my_func(arr);  //just pass the name of the array

and the following rarely considered and the syntax even more rarely achieved

C++
void my_func1(int(&a)[10]) // a reference to an int[10]
{
    //do things with a , an int[10]
}

int arr[10];
my_func(arr);  //just pass the name of the arrat here too

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:

C++
//Get number of elements in first rank of any array
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

C++
//Get elements at index with bounds checking
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");
}

//Get elements at const index with compile time bounds checking
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]

Emulating class method semantics for plain arrays

The bounds checked access function described above

C++
int i1 = get_at(arr, index);

is only slightly uglier than the class method call you get with std::array

C++
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

C++
int aaa[10][10][10];

int res1 = aaa[3][4][5]; //normal access

int res2 = get_at(get_at(get_at(aaa, 4), 5), 3);//bounds checked access

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:

C++
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

C++
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:

C++
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.

C++
inline auto at(size_t index)
{
    return at_index(index);
}

and now we can write:

C++
int a[10];
int res = a->*at(7);

and 

C++
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. 

C++
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:

C++
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

C++
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.

C++
template <size_t I>
inline constexpr auto at()
{
    return at_const_index<I>();
}

So we can write:

C++
int a[10];

int res = a->*at<7>();

and

C++
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.

C++
template<char ... Args>
constexpr auto operator "" _th()
{
    return at_const<int_from_char_args<Args...>()>();
}

and now we can write:

C++
int a[10];

int res = a->*7_th;

and with a multidimensional array:

C++
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:

C++
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':

C++
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.

Names

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:

C++
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:

C++
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:

C++
using array_methods::at;
using array_methods::operator""_th;

int arr[10][10][10];

arr->*array_methods::fill(0); //fill wasn't promoted so has to be qualified with aray_methods::
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.

C++
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:

C++
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  

C++
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 

C++
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.

Using the code

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'

C++
#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:arrays and std::vectors 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'.

C++
void my_func1(int(&arr)[10][10]) // a reference to an int[10][10]
{
    //fill
    arr->*fill(4);        //fills every element of volume with 4
    
    //this works too with plain arrays
    for (auto& ar : arr)    
        for (auto& e : ar)
            e *= 2;        //doubles every element of volume

    //bounds checked element access
    int res1 = arr->*4_th->*5_th;        //compile-time bounds checked access

    int res2 = arr->*at(res1)->*at(res1); //run-time bounds checked access

    //iteration of first rank - iterates through int[10]s held by first rank
    auto iter = arr->*get_begin();
    auto end = arr->*get_end();
    for (iter; iter != end; iter++)
        (*iter)->*4_th *= 3;      //trebles the 4 th element of each int[10]

    //volume iteration - iterates through every stored element in the volume
    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;      //trebles every element of volume.
}

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 methods reference

Quick reference and index

//_______________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

Promotion to global namespace

Either

C++
using namespace array_methods; //promotes all methods

or (comment out the ones you don't want to promote)

C++
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 property methods

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 

C++
int a[10];
size_t n1 = a->*get_size(); // n1 will be 10

int aaa[10][10][10];
size_t n2 = aaa->*get_size(); // n2 will be also be 10

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.
C++
int a[10];
size_t n1 = a->*get_rank(); // n1 will be 1

int aaa[10][10][10];
size_t n2 = aaa->*get_rank(); // n2 will be be 3

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.
C++
int a[10];
size_t n1 = a->*get_volume(); // n1 will be 10

int aaa[10][10][10];
size_t n2 = aaa->*get_volume(); // n2 will be  1000

Note: For fixed sized arrays get_volume() is a simple calculation that can be carried out during compilation. However if std::vectors are part of the volume, it is an extensive recursive iteration that has to be carried out at run-time.

Element access methods

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.

C++
size_t i;

for(i= 0; i != array->*get_size(); i++)  //control loop limits the bounds of i
      array[i] = 6;     //bounds checking here would be an unnecessary overhead

array->*at(index)     element access with run-time bounds check.
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.
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  

C++
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: 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:

C++
array->*at(x)->*at<false>(y)->*at(z)

instead of

C++
(array->*at(x))[y]->*at(z) //() are required because [] has a higher precedence than ->*

array>*at<INDEX>() compile-time bounds check with symbolic constant
Operates on: plain array, std::array or std::vector
Takes: 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.

C++
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.

C++
array->*4_th->*5_th->*6_th

For std::vector this will decay to a run-time bounds check.

Copy swap and fill methods

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:

C++
int a[10][15];
int b[10][15];
//.....fill code
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::arrays, std::vectors or a multidimensional hybrid. However any std::vectors 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

C++
int arr[3][3];

arr->*fill(5); //fill every element of the volume

producing
5, 5, 5
5, 5, 5
5, 5, 5

C++
arr->*fill({1, 2, 3})  //fill each row of the first rank array

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.

 

Iterator methods

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:

C++
auto iter = arr->*get_begin();
auto end = arr->*get_end();

for (iter; iter != end; ++iter)
	*(iter) += 5; //add 5 to each element

 

Reverse iterator methods

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:

C++
auto iter = arr->*get_rbegin();
auto end = arr->*get_rend();

for (iter; iter != end; ++iter)
	*(iter) += 5; //add 5 to each element

Note that the algorithm is that of a forward iteration. It becomes a reverse iteration simply by passing it reverse iterators.

Volume iterator methods

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:

C++
int arr[10][10];

auto iter = arr->*get_begin_volume();
auto end = arr->*get_end_volume();

for (iter; iter != end; ++iter)
	*(iter) += 5; 
//add 5 to each of the 100 stored data elements in the volume

 

Reverse volume iterator methods

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:

C++
int arr[10][10];

auto iter = arr->*get_rbegin_volume();
auto end = arr->*get_rend_volume();

for (iter; iter != end; ++iter)
	*(iter) += 5; 
//add 5 to each of the 100 stored data elements in the volume

Note that the algorithm is that of a forward iteration. It becomes a reverse iteration simply by passing it reverse iterators.

Insecure cast reference

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.

C++
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.

Summary

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::arrays and std::vectors 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.

History

First release: 04 Jan 2018.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Retired
Spain Spain
Software Author with engineering, science and mathematical background.

Many years using C++ to develop responsive visualisations of fine grained dynamic information largely in the fields of public transport and supply logistics. Currently interested in what can be done to make the use of C++ cleaner, safer, and more comfortable.

Comments and Discussions

 
-- There are no messages in this forum --