Introduction
This article is a good practice for learning templates and a bit about design.
Background
As a young programmer (and even now), I make a recurring mistake: When managing an array of devices, usually the device ID ranges from 1 .. N and the array holds the devices ranging from 0 .. N-1; this simple offset often manufactures a problem. Many times, you would see code as follows:
m_ArrDevices[pDevice->DeviceId] ->DoSomething()
The problem with this line is that the device ID is 1 but the position in the array is supposed to be 0 and instead, we are trying to access the device in the second position. The most common and easy solution is to subtract 1 from the DeviceId
whenever using it but this solution is not error-free and from time to time, looks hideous. Another question arises is what happens when the two worlds (the device ID and the device position) are not so easy to transform from? What if it's a very complex hashing mechanism? Whose responsibility is it to translate the ID to the position?
Analyze This
Let's take this simple case and examine it a bit:
const int NUMBER_OF_DEVICES = 90;
class cDevice{}; cDevice arrDevices[NUMBER_OF_DEVICES];
cDevice& returnDevice(int DeviceNumber)
{ return *(arrDevices[DeviceNumber-1]);
}
int main()
{
cDevice* tmpDevice= arrDevice[0];
returnDevice(tmpDevice->DeviceID).Work();
return 0;
}
This simple example arises the most important question of all: Do we even need to search for a way to generalize this problem? As you can see in (2), the problem is fixed! The answer is that it really depends on the big picture: if that was the only time you would need to adjust the offset of the devices, you can relax, or extract it into a function and maybe change it when needed. But if it's a part of a 20 men project, working for a long period of time or with a changing offset adjusting function or even a hash function, you can be sure that someone will forget to apply it, misuse it, or even over use it! So you need a mechanism that would enforce the transformation but you would want it to be transparent and flexible. Let's get to work. First, let us analyze our example:
- Our
DeviceNumber
is int
but it could vary, from a P.O.D. type to a class. - Our offset adjusting function could be everything from a simple mathematical equation to a complex hashing function.
- In this example, we receive an
int
:
- apply a mathematical operation
- that returns us an
int
- again, but what if we have a dictionary that receives a string, applies a hashing function on it that returns a class of indexes?
We need to generalize the type of the receiving value, the transformation function, and the index type. Considering these points, we can already picture our template:
template < typename RetPar, typename TypePar , class FCPar >
class transformationTemplate
{
};
In the above example, the specialization of transformationTemplate
will be transformationTemplate<int,int, blank>
(later we will fill in the blank). If we want the transformation to be transparent, we need it to be able to behave as the type it encapsulates, in our example: int
; so let's add to our class the functions needed:
- copy constructor that receives a value of
RetPar
. - copy assignment operator for the value of
RetPar
. - the rule of three states that if you explicitly define the two above, you must define the destructor too.
Our code will look like:
template < typename RetPar, typename TypePar , class FCPar >
class transformationTemplate
{
public:
transformationTemplate(TypePar const& newValue):m_value(newValue) {}
TypePar const& operator =
(TypePar const& newValue) { m_value = newValue; return newValue; }
virtual ~transformationTemplate() {}
private:
TypePar m_newValue;
};
(A,B) The copy constructor and assignment operator gives us the ability to hide from the user the fact that he is using a class and not the data type he previously was using; we are passing the variable by ref const to TypePar
(TypePar const& newValue
) so we could use variables and <int,int,blank>
literals.
<int,int,blank>int i = 0; returnDevice(i); returnDevice(0);
(C) In the specific example, there is no need to give the destructor a special implementation but adding the virtual identifier will help us when we will want to inherit from this class.
Now let's attack the returning type RetPar
. We want our convertor class to be transparent in the return side as well! So we add a cast operator to the RetPar
data type.
virtual operator RetPar()
{
return m_value -1; }
Let's see how it works!
typedef transformationTemplate<int,int,FCPar><int,> tIntConvertor;
const int NUMBER_OF_DEVICES = 90;
class cDevice{}; cDevice arrDevices[NUMBER_OF_DEVICES];
cDevice& returnDevice(tIntConvertor DeviceNumber)
{ return *(arrDevices[DeviceNumber]);
}
int main()
{
cDevice* tmpDevice= arrDevice[0];
returnDevice(tmpDevice->DeviceID).Work();
return 0;
}
But wait, that's not all. We need to extract the transformation function so we could change it whenever we want. The method of extracting a part of your algorithm and encapsulating it is called Trait. As I said before, the converter function should be able to receive a type and return a new type:
template< class RetPar , class TypePar >
class cTransFuncBinary
{
public:
virtual RetPar operator ()(TypePar value) = 0;
};
This is the prototype for every transformation class from now on. For our example, we will need this class:
class cTransFuncMinus : public cTransFuncBinary<int>
{
public:
int operator()(int value)
{
return value - 1;
}
};
Back to the class, let's "embed" the cTransFuncMinus
class into transformationTemplate
. The transformation should take place in the cast to int function.
virtual operator RetPar()
{
FCPar apply;
return apply(m_iData);
}
- Instead of instantiating
FCPar
in the RetPar
function, you can hold a member to a pointer and use it to change the FCPar
member at run time. I chose not to. (See the Strategy Design Pattern.) - I decided to encapsulate it as a class and not as a function because it would be easier for me to extend it later.
After adding all of the components, our code should look like:
typedef transformationTemplate< int , int , cTransFuncMinus ><int,> tIntConvertor;
const int NUMBER_OF_DEVICES = 90;
class cDevice{}; cDevice arrDevices[NUMBER_OF_DEVICES];
cDevice& returnDevice(tIntConvertor DeviceNumber)
{ return *(arrDevices[DeviceNumber]);
}
int main()
{
cDevice* tmpDevice= arrDevice[0];
returnDevice(tmpDevice->DeviceID).Work();
return 0;
}
The source
In the attached project, I played with the possibilities of the transformation template. Feel free to browse the code and play with it.