I'm writing an interpreter in C++ for a lisp-like language of my humble design. This is for fun and for learning, so I'm not after absolute efficiency. But I am trying to have a very clean C++ code. I'm currently wondering how to implement builtin functions.
Basically, what I do is this :
I have an abstract base class DataObject which just provides type information (currently double, int, bool) and is inherited by the specific data containers, like :
class DataObject
{
public:
virtual const Type *type() = 0;
};
template<class T, const Type * myType>
class DataObjectValue : public DataObject
{
T value;
public:
const Type *type(){return myType;}
};
But then, when I want to perform say an addition I have to do things like :
DataObject * sum(DataObject *a, DataObject *b)
{
if(a->type() == &Integer and b->type == &Integer)
{
DataObjectValue<int>* ia = dynamic_cast< DataObjectValue<int>* >(a);
DataObjectValue<int>* ib = dynamic_cast< DataObjectValue<int>* >(b);
return new DataObjectValue<int>(ia->value+ib->value);
}
else if(a->type() == &Real and b->type == &Real)
{
DataObjectValue<double>* ra = dynamic_cast< DataObjectValue<double>* >(a);
DataObjectValue<double>* rb = dynamic_cast< DataObjectValue<double>* >(b);
return new DataObjectValue<double>(ra->value+rb->value);
}
else...
}
Which gets pretty annoying pretty quickly (do that for - * / < <= >= < > ....) and for several other types. This is hard to maintain. Of course I have simplified as much of the process as I could think of by introducing lots of templates everywhere, but still, I can't help but to think there must be a cleaner way. Do you a) see what my problem is (I doubt my explaining, not your competence) b) have any suggestion?
The current implementation that you have basically performs type erasure in the exact type that is being stored, and you are doing it in a slightly unusual way (why don't you use an enum rather than pointers to unique objects?)
I would start by providing promotion operations to the base class, and have them implemented in each level:
enum DataType {
type_bool,
type_int,
type_double
};
struct DataObject {
virtual ~DataObject() {} // remember to provide a virtual destructor if you
// intend on deleting through base pointers!!!
virtual DataType type() const = 0;
virtual bool asBool() const = 0;
virtual int asInt() const = 0;
virtual double asDouble() const = 0;
};
Then you can implement the operations in a simple functor:
template <typename T>
T sum_impl( T lhs, T rhs ) {
return lhs + rhs;
}
And provide a simple dispatch function:
DataType promoteTypes( DataType lhs, DataType rhs ) {
if ( lhs == type_double || rhs == type_double ) {
return type_double;
} else if ( lhs == type_int || rhs == type_int ) {
return type_int;
} else {
return type_bool;
}
}
template <template <typename T> T operation (T,T)>
DataObject* perform_operation( DataObject* lhs, DataObject* rhs, operation op ) const {
DataType result_type = promoteTypes( lhs->type(), rhs->type() );
switch ( result_type ) {
case type_double:
return new DataObjectValue<double>( op( lhs->asDouble(), rhs->asDouble() );
case type_int:
return new DataObjectValue<int>( op( lhs->asInt(), rhs->asInt() );
case type_bool:
return new DataObjectValue<bool>( op( lhs->asBool(), rhs->asBool() );
default:
abort();
}
}
With all the pieces in place, you can implement the operations almost trivially, by just providing a function template for the specific operation (as the sum
above), and then using the rest of the places:
// sum_impl as above
DataObject* sum( DataObject* lhs, DataObject* rhs ) {
return perform_operation( lhs, rhs, sum_impl );
}
Now that is just the pattern I would use, but I would make some changes, I prefer to use as few pointers as possible, which means that I would not pass the arguments by pointer but rather by reference. Also, I would do proper type erasure (take a look at boost any) and make Object
a complete type that contains DataObject
elements, and then perform the operations on that type (rather than the hierarchy). That will enable you to provide functions that also return by value (and hide the dynamic memory allocations internally, which also means that resource management can be controlled inside Object
and is not the responsibility of user code). With those changes, you can reuse and simplify the structure above and provide a cleaner solution.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With