MQL's OOP notes: rubber arrays, safe numbers, exceptions handling and other tricks: part 2

Unfortunately exceptions are missing in MQL. Which is why we're going to implement a surrogate, and have already prepared helper classes of rubber arrays and safe numbers in the previous publication. These classes will provide a playground for implementing our own exception handlers.
Exceptions
{
// protected code
}
catch(Exception e)
{
// code to handle exception
}
// other code
Many OOP languages provide very similar syntax.
Our exception handler will work slightly different, because MQL does not provide much freedom in what we can do in general.
{
static try *landmarks[];
static int cursor;
string location;
The object contains a variable of type string: an identifier of location. Also the class contains the static array landmarks where we will save references for all instances of try objects, and cursor which points to specific try (if any), containing lines of code being executed (of course, try blocks may not cover all the code, and there can be moments of executing code outside any try block). We need to initialize the static stuff outside the class definition.
int try::cursor = -1;
And then get back to the class, specifically to its constructor.
try(const string mark)
{
location = mark;
int n = ArraySize(landmarks);
ArrayResize(landmarks, n + 1);
landmarks[n] = &this;
Print("try at ", mark, " ", n);
}
The contructor accepts a single parameter with an identifier. Then current instance - this - is added to the array of landmarks. We don't need destructor, which could remove the reference from the array, because all instances of try will be static. This makes sense since the objects are landmarks ;-). They will be created automatically by the platform before program initialization. But we need to know when a specific try block becomes active, so add appropriate method.
{
int back = cursor;
int size = ArraySize(landmarks);
for(int i = 0; i < size; i++)
{
if(landmarks[i] == &this)
{
cursor = i;
return back;
}
}
return back;
}
Here we search through the array of landmarks and set cursor to found index. In addition we return previous value of the cursor, making it possible to save and restore code execution context.
{
cursor = rollback;
}
Who will switch the context, you ask? This should be objects of another class, and they can not be static this time, because they should be created and deleted locally and ad hoc, according to control flow, that is how program enters and leaves functions, loops and other code blocks defined by curly brackets. Here is the class.
{
private:
int restore;
try *ptr;
public:
context(try &_try)
{
ptr = &_try;
restore = ptr.activate();
}
~context()
{
ptr.restore(restore);
}
};
Every time an object context is created, it accepts a reference to nearby try block landmark and calls its activate method. As soon as it's deleted (when control flow leaves current code context), the index of previously active landamrk is restored.
static try _try(__FILE__ + ", " + (string)__LINE__ + ", " + __FUNCSIG__); context _context(_try);
}
Now we're approaching most tricky point of the scheme. Having a try block identified and activated, we need to intercept and handle exceptions somehow. The interception part is relatively simple. In the previous publication we created classes of rubber arrays and safe numbers, which can intercept specific errors, such as "index out of bounds" and "divide by zero". So, they can throw an exception and pass it to our try block. All we need is a method accepting the exception. Lets add it to the try class.
{
int size = ArraySize(landmarks);
if(cursor >= 0 && cursor < size)
{
// TODO: invoke an error handler
}
return 0;
}
Having this method we need to jump for a moment back to the classes RubbArray and Number to refine their methods controlling possible errors. For example, RubbArray could have:
{
if(i < 0 || i >= ArraySize(data))
{
return try::throw("Read index out of bounds: array size=" + (string)ArraySize(data) + ", index=" + (string)i);
}
return data[i];
}
The method throw does nothing at the moment. Some error handler is required. This could be just a global function, for example OnError, but it would be better to try to mimic catch block syntax as much as possible, and define handlers locally for every block. In comparison with a single global function, this approach will give one more benefit of splitting error handling code to small parts, specific for every particular error and location.
{
public:
catch(try &_try)
{
_try.bind(this);
}
virtual void handle(Exception &e)
{
// just dump the error by default
}
};
Of course, the catch object should be bound to try, this is why constructor accepts a reference for one. But what is the method bind? We don't have it yet. Lets add it to the class try.
{
catch *ptrc;
public:
void bind(catch &c)
{
ptrc = &c;
}
And now it's time to elaborate the method throw.
{
int size = ArraySize(landmarks);
if(cursor >= 0 && cursor < size)
{
if(CheckPointer(landmarks[cursor].ptrc) != POINTER_INVALID)
{
Exception e(landmarks[cursor].location, message);
landmarks[cursor].ptrc.handle(e);
return e.result;
}
}
return 0;
}
Here we use another undefined yet class - Exception. It's that is easy to conceive.
{
public:
const string location;
const string message;
double result;
Exception(string l, string m): location(l), message(m), result(0) {}
void throw(const double r)
{
result = r;
}
};
This is just a storage of all information about current error, and also a value, which the handler can pass via Exception object back to protected code, as a result of error processing. The member variables made public for simplicity.
{
public:
catchx(try &_x): catch(_x) {}
virtual void handle(Exception &e)
{
Print(e.location, " : ", e.message);
e.throw(EMPTY_VALUE);
}
}
This example just prints information about the error and returns EMPTY_VALUE to protected block of code. If, for example, the origin of the error is a line trying to access a rubber array element by incorrect index, then it will get EMPTY_VALUE as a result, and the code continue execution smoothly. This is the difference between conventional exception handlers and our MQL exception handler. We don't have a reliable way to jump out of the block in pure MQL.
#define CATCH(A) class catchx:public catch{public: catchx(try &_x):catch(_x){} virtual void handle(Exception &A)
#define END }; static catchx _catchx(_try); }
Cryptic? Well, this may take a while to understand (though all parts have been already described above), but what it actually means - we can now write something like this:
Number<double> x(0);
Number<double> y;
TRY
{
y = d / x; // no error here, and y is DBL_MAX
Print(x.toString(), ", ", y.toString());
}
CATCH(e)
{
Print(e.location, " : ", e.message);
e.throw(DBL_MAX); // pass DBL_MAX as result of division by 0
}
END
Magic? No - OOP.
The complete code of exception handling classes is attached below in the file Exception.mqh. Also an example script exception.mq4 is provided. Its output is:
try at exception.mq4, 61, void OnStart() 1
try at exception.mq4, 25, void subfunc() 2
try at exception.mq4, 40, void subfunc2() 3
initialized
exception.mq4, 25, void subfunc() : Divide by zero
0, 1.797693134862316e+308
exception.mq4, 61, void OnStart() : Write index out of bounds: array size=2, index=3
exception.mq4, 56, void OnStart() : Read index out of bounds: array size=2, index=3
15.0
exception.mq4, 40, void subfunc2() : Divide by zero
0
Have fun.