Saturday, August 2, 2008

c++, elegant static arrays

It can be better
I will show you a nicer way of 'declaring' c++ static arrays that is more elegant, a bit less error prone, and makes life easier when changing the array size or index type.
This is fairly basic, and I have been using it for a long time, but I have never seen any code or tutorial doing it my way.

In any case, its a really microscopic issue that does not deserve so many words, but only needs a nice small example.

The basic Tutorial
this is tutorial code from the 1st search engine hit for 'tutorial c++ static arrays'

PS: when viewed with IE, the source code is not horizontally scrollable, I am too lazy to find out why, sorry, use firefox.

int my_array[10];

my_array[0] = 100;
my_array[1] = 100;
my_array[2] = 100;
my_array[3] = 100;
my_array[4] = 100;
my_array[5] = 100;
my_array[6] = 100;
my_array[7] = 100;
my_array[8] = 100;
my_array[9] = 100;

for(int i = 0; i<10;>
my_array[i] = 100;
ok, this was your standard tutorial, from my point of view though, it should be like this.

template<typename TypeT, size_t LengthT, typename PreferredIndexT = int>
struct StaticArray {

typedef int EnumType; //this is an int, except if changed in compiler settings when available
typedef PreferredIndexT PreferredIndex;
typedef TypeT Type;

enum { Length = LengthT };
Type data[Length];

inline StaticArray() {

//check if Length fits into enum data type
numeric_cast<EnumType>(LengthT);
}

static inline EnumType length() const { return Length; }

template<typename OutT, typename InT>
static inline OutT numeric_cast(const InT& val) { assert(((InT) (OutT) val) == val); return (OutT) val; }

template<typename T>
static inline const T& getLength() const { static const T len = numeric_cast<T>(length()); return len; }

template<typename T>
inline Type& safeEl(const T& i) { assert(i >= 0 && i < Length); return data[i]; }

template<typename T>
inline const Type& ctSafeEl(const T& i) const { return safeEl(i); }


template<typename T>
inline Type& el(const T& i) { return data[i]; }

template<typename T>
inline const Type& ctEl(const T& i) const { return el(i); }


template<typename T>
inline Type& operator[](const T& i) { return el(i); }

template<typename T>
inline const Type& operator[](const T& i) const { return ctEl(i); }

typedef Type* iterator;

inline iterator begin() const { return data; }
inline iterator end() const { return data + Length; }
};
usage, with comments explaining some of the benefits:
{
typedef StaticArray<int, 300> Array;
Array arr;

//we can do away with the typedef
//StaticArray<int, 300>arr;

//we can sepcify the preferred index type to be used with this array
//StaticArray<int, 300,unsigned short> arr;

{
int i = 0;
arr[i++] = 0; //use i++ and save us from typo problems, we also can copy-paste better like this
arr[i++] = 1;

arr[i++] = 2;
arr[i++] = 3;
arr[i++] = 4;
arr[i++] = 5;
arr[i++] = 6;

arr[i++] = 7;
arr[i++] = 8;
arr.safeEl(9) = 9; //check that we did not go over the array's size
}

//with arr.length(), we don't need to change this code if the array size changes
for(int i = 0; i < arr.length(); ++i)
arr[i] = i;

//this will produce an assert if 'char' cannot hold the size of the array, that's good!
for(char i = 0; i < arr.getLength<char>(); ++i)
arr[i] = i;

//only works with typedef declaration
//use the 'preferred' index
for(Array::PreferredIndex i = 0; i < arr.getLength<Array::PreferredIndex>(); ++i)
arr[i] = i;

//use an stl type iterator, if we ever use an stl container, no code
//needs to change
//additionally, we do not to worry about index types
{
Array::Type i = 0;

for(Array::iterator it = arr.begin(); it != arr.end(); ++it) {

*it = i++;
}
}

//the 'best' version works only with c++0x
//using auto allows changing the iterator (or index type if not using iterator)
//without needing to change code anywhere else
{
Array::Type i = 0;

for(auto it = arr.begin(); it != arr.end(); ++it) {

*it = i++;
}
}
}

Evolution
Agreed, this might be too heavy for a 'static arrays' tutorial, BUT the problem is most people don't evolve and don't strive to improving their skills, this approach works so they stick to it.
I have seen senior programmers with 10+ years experience still sticking to it, there's definitely nothing wrong with that, but there is a certain general 'attitude' in play here, being the 'non-naive flexible' perfectionist I am, I cannot help but to keep pushing the limits, and this is the latest version to date.

Benefits
One thing to note is that all this fanciness introduces zero overhead, by using inline functions and static/const variables.

Also notice that this really 'decouples' the array declaration from the code using it, making changes to array properties (size, data types, preferred index) automatically propagate through our code with no need to change anything, and whenever changes are needed but cannot be detected at compile time, asserts will come to the rescue and save us many headaches.
I like this because I have a tendency to try to write 'fire and forget' code as much as c++ allows, this means using asserts to warn me whenever there is danger that things might break at run time.
The extremely simple but effective numeric_cast is an example of that, making sure all data types used to store indexes are 'fire and forget' so that I do not to be paranoid, allowing me to make changes with piece of mind knowing that I'll directly get notified when there is danger lurking around the corner, there maybe room for a more detailed explanation here, but I will leave it at that.

Long Term Thinking
In the end, many will probably dismiss this as pointless overkill, in my personal opinion and experience, in the long term it never is.

No comments: