Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Templated Convertor and Hasher

1.27/5 (6 votes)
17 Aug 2008CPOL4 min read 1   53  
A bit about templates and design.

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:

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

C++
const int NUMBER_OF_DEVICES = 90;
class cDevice{};     //A dummy
cDevice arrDevices[NUMBER_OF_DEVICES];
                      //(1)
cDevice& returnDevice(int DeviceNumber)
{                     //(2)(3)
    return *(arrDevices[DeviceNumber-1]);
}
int main()
{
    cDevice* tmpDevice= arrDevice[0];
     // Do things with the Device
                            //(4)
    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:

  1. Our DeviceNumber is int but it could vary, from a P.O.D. type to a class.
  2. Our offset adjusting function could be everything from a simple mathematical equation to a complex hashing function.
  3. In this example, we receive an int:
    1. apply a mathematical operation
    2. that returns us an int
    3. 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:

C++
//remeber! there is no difference between
//class and a typename in a template definition
template < typename RetPar, typename TypePar , class FCPar >    
class transformationTemplate
{
// class implementation
};

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:

  1. copy constructor that receives a value of RetPar.
  2. copy assignment operator for the value of RetPar.
  3. the rule of three states that if you explicitly define the two above, you must define the destructor too.

Our code will look like:

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

C++
<int,int,blank>int i = 0;    returnDevice(i);    //Variables 
returnDevice(0);                  //Literals

(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.

C++
virtual operator RetPar()
{
    return m_value -1; //Just temporary
}

Let's see how it works!

C++
//I'm ignoring the FCPar for now
typedef transformationTemplate<int,int,FCPar><int,>  tIntConvertor;    
const int NUMBER_OF_DEVICES = 90;
class cDevice{};     //A dummy
cDevice arrDevices[NUMBER_OF_DEVICES];
                                  
cDevice& returnDevice(tIntConvertor DeviceNumber)
{                                //The casting oprator substracts 1 for us
    return *(arrDevices[DeviceNumber]);
}
int main()
{
    cDevice* tmpDevice= arrDevice[0];
     // Do things with the Device

    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:

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

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

C++
virtual operator RetPar()
{
    FCPar apply; 
    return apply(m_iData);
}
  1. 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.)
  2. 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:

C++
//I'm ignoring the FCPar for now
typedef transformationTemplate< int , int , cTransFuncMinus ><int,>  tIntConvertor;    
const int NUMBER_OF_DEVICES = 90;
class cDevice{};     //A dummy
cDevice arrDevices[NUMBER_OF_DEVICES];
                                  
cDevice& returnDevice(tIntConvertor DeviceNumber)
{      //The casting oprator calls the cTransFuncMinus
    return *(arrDevices[DeviceNumber]);
}
int main()
{
    cDevice* tmpDevice= arrDevice[0];
     // Do things with the Device

    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.

License

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