Friday, 8 August 2008

C++ > Storing heterogenous objects in a container

While designing in C++, many times I got stuck to get a container which can store objects of multiple types. Fortunately for me I always managed to find an alternative design in which there was no such need. Recently, I got stuck and I *had* to find a way to do this. This was the situation:

There is an interface( or class with all pure virtual functions) ITask. I wanted to create many task classes which adhere to this interface. The project demanded that the task objects to be created and used (and executed) in a uniform manner. Since every task might take different type and number of arguments, I had to find a way to create an container which can store objects of different types. The problem is that STL containers like vector can store objects of only one type or of derived classes of the type for which the vector was created.

In terms of code I wanted something like this:

// Lets say I have a task which creates report
// by accessing tables.
// So the inputs to the task are:
// 1. Database connection
// 2. Output file name

class CreateReport : public ITask
{
public:
// I did not want seperate class like ArgumentList for
// each Task class. The same class should be used to pass
// the arguments for all task classes. This is what I mean
// by "uniform manner" mentioned earlier.
void run(const ArgumentList* args) // Inherited from ITask
{
...
DBConnection* conn = args->getValue("connection");
string fileName = args->getValue("filename");
...
}
...
};

// Using CreateReport
...
ArgumentList* a = new ArgumentList();
a.add(new DBConnection(connectionString));
a.add("report.txt");
ITask* t = new CreateReport();
t->run(a);



When I came up with a solution, it was very obvious and I thought "Why could'nt I think this before?"

Solution >>

Now, if you know Boost, you must be thinking "Well, there are tuples which can be used to store multiple types of objects". The issue is that in my case I don't know at compile time how many arguments are going to be passed, so using tuple is not an option. I will discuss pros and cons of my solution & tuples later below. First the solution:


// Interface
class IArgument
{
public:
virtual string getName() = 0;
virtual ~IArgument(){}
};

template
class Argument : public IArgument
{
public:
Argument(string i_name, VALUE_TYPE i_value)
:m_name(i_name), m_value(i_value)
{
}

/// Get the argument name
virtual string getName() { return m_name; }

VALUE_TYPE getValue() { return m_value; }

protected:
string m_name;
VALUE_TYPE m_value;
};

class NamedArguments
{
public:

/// Gets the value by name
template
VALUE_TYPE getValue(string i_argName)
{
map::iterator itr;

if( (itr = m_argMap.find(i_argName)) == m_argMap.end() )
THROW_AMR_EXCEPTION_2(ArgumentNotFound, i_argName);
Argument* arg = NULL;
if( (arg = dynamic_cast< Argument* >(itr->second)) == NULL)
THROW_EXCEPTION(InvalidArgumentType);

return (VALUE_TYPE)arg->getValue();
}

/// Adds the argument and value
template
void add(string i_argName, VALUE_TYPE i_value)
{
map::iterator itr;

// Ignore if the argument is already present
if( (itr = m_argMap.find(i_argName)) != m_argMap.end() )
return;

m_argMap.insert
(
map::value_type
(
i_argName, new Argument(i_argName, i_value)
)
);

//m_argMap[i_argName] = new Argument(i_argName, i_value);
}

/// DTor

~NamedArguments()
{
for(map::iterator itr = m_argMap.begin();
itr != m_argMap.end(); itr++ )
delete itr->second;
}

private:

map m_argMap;
//map m_argNames;
};

//
// And ... this is how its used??
//

NamedArguments n;
n.add<DBConnection*>("connection", conn);
n.add<string>("filename",string("report.txt"));

CreateReport r;
r.run(&n);

// ... The CreateReport::run() will look like
void CreateReport::run(const NamedArguments* args)
{
...
DBConnection* conn = args->getValue<DBConnection*>("connection");
string filename = args->getValue<string>("filename");
...
}



Pros and cons of this solution and tuple to follow...