I’m working with 3D arrays that I flatten into a 1D arrays with the following index calculation:
index = x + WIDTH * (y + DEPTH * z)
I’m starting to notice that I’m using the calculation all over the place in different classes and am wondering if using a macro would be a good solution?
I’ve also thought about creating a static method in a class and then using that in all these classes, but adding that dependency to all those classes doesn’t sit well with me.
Is there a good standard practice or pattern to follow in this case?
1
Given that your question is tagged c++, do not use macros! They should only be used for include guards and a few other esoteric uses.
The proper solution here is a standalone inline function. Assuming that WIDTH
and DEPTH
are constants that are visible to the function, here is its definition:
inline int f(int x, int y, int z) {
return x + WIDTH * (y + DEPTH * z);
}
Place this function in a header accessible by all of the classes that use it. I would put it in a header with utility functions for whatever namespace it is part of. If such a header does not exist, this might be a good time to create it. Regardless, it must go in a header so the function definition is visible to each compilation unit that includes a call to the function: in other words, the compiler has to be able to see the function to put its contents inline with the calling code.
This solution centralizes the code, providing all of the benefits of a function. Since it is inline, it removes the function call overhead.
Most importantly, it provides well-defined semantics about its parameters:
index = f(x++, ++y, z);
This is well-defined: we can look up how this works in the C++ standard. If it were a macro, it may work as expected in a simple case but it can break down in more complex macros (beyond the scope of this question).
2
but adding that dependency to all those classes doesn’t sit well with me.
If each array is encapsulated inside a data class, it makes sense to add an overloaded operator to that class to lookup the elements.
// This code only illustrate the overloaded operator.
// The code here is incomplete, and by itself is unfit
// for all purposes.
//
// (See below for a link to OpenCV's matrix source code.)
//
template <typename T>
class Array2
{
T& operator () (int x, int y) { return mData[y * mWidth + x]; }
const T& operator () (int x, int y) const { return mData[y * mWidth + x]; }
// If one really needs a function to just return the value of (y * mWidth + x), such functions can also be added to this class.
private:
int mWidth;
int mHeight;
T* mData; // make sure you know how to allocate, free, prevent memory leak, and prevent double-freeing of this pointer to array.
};
A more fully implemented version will probably look like this:
- OpenCV Core mat.inl.hpp, Lines 1476-1494,
Mat_<Tp>::operator()(...)
If there are multiple classes, and it is foreseeable in the near future that each class may soon have different dimensions (say, 2D and 4D), and if you are not already using a class for each kind of array, you should seriously consider making that transition.
If, however, you must use a free function (not a member of any class), keep these issues in mind:
-
Think about how
WIDTH
andHEIGHT
are passed into the function. In the code sample (and also in @Snowman’s answer), it is assumed that your entire program will have a single hard-coded value forWIDTH
andHEIGHT
. As soon as you start using arrays of different sizes, you will see that the data class encapsulation approach is the only sane way to do that. -
If you need to use arrays of different dimensions, give distinct names to those functions.
-
Avoid function name collisions. If you make that a class member, there is less worry of function name collisions. Otherwise, you can make that a static class member, give that function a project-specific prefix, or put that function in a namespace.
Finally, it is okay to have duplications for such small pieces of code. Avoidance of code duplication should be weighed against the problem of increasing dependencies (and hence coupling). In this particular case, it is favorable to let each class own the convenience functions it uses.
Of course, the encapsulated data class approach should still be the most favorable approach, when all things are considered.
If in the very far future you start considering implementing SIMD and other performance optimizations, further changes to the data class will be needed.
1
Aside from using a free function, you may want to examine your class decomposition. If you have a lot of classes which have a 3D matrix flattened into a 1D, and they all access that matrix via this calculation, it’s likely that they should all have a common class threeDinOneD
which they use.