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

8 November 2016, 14:54
Stanislav Korotky
8
476
Exception handling is a powerfull and useful mechanism of modern OOP. It's used to improve program stability, manage resources, change control flow on-the-fly, simplify applied source code, which otherwise - without exception handling - is usually cluttered up with tons of explicit checkups preceding almost every variable access.

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

Typical exception handling block in C++ has the following structure:

try
{
  // protected code
}
catch(Exception e)
{
  // code to handle exception
}

// other code

Many OOP languages provide very similar syntax.

If an error occurs in the try block, it intercepts the error and passes control to the catch block which can analyse current situation and decide if the program can recover and how. The execution of protected code is stopped on the line where the error happened, but if the handler is able to mitigate the problem, the execution resumes on the lines below the catch block. 

Our exception handler will work slightly different, because MQL does not provide much freedom in what we can do in general. 

First, we need a technique of marking parts of code as try blocks. Objects of some class can do the trick. So, let us define the class try

class try
{
  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.

try *try::landmarks[];
int try::cursor = -1;

And then get back to the class, specifically to its constructor.

  public:
    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 activate()
    {
      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.

    void restore(int rollback)
    {
      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. 

class context
{
  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.

In a program, the pair of try and context objects can be declared in the following way:

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

    static double throw(const string message)
    {
      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:

    T operator[](int i) const
    {
      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.  

It's very logical to define the handler as a method of a class. 

class catch
{
  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.

class try
{
  catch *ptrc;

  public:

    void bind(catch &c)
    {
      ptrc = &c;
    }

And now it's time to elaborate the method throw.

    static double throw(const string message)
    {
      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.

class Exception
{
  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.

We're almost done. Let's see how an object with error handler could look. 

class catch22: public catch
{
  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.  

For convenience we define the following macros, which simulate try-catch blocks via declaration of all abovementioned objects in a row.

#define TRY { static try _try(__FILE__ + ", " + (string)__LINE__ + ", " + __FUNCSIG__); context _context(_try);
#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> d(10.5);
  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. 

You may ask why do we need the separate class catch, if we could define the handler inside try? The answer is simple: because we want to place catch blocks below protected code. If it would be a part of try, the handler should be defined on top of the block.

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, 56, void OnStart() 0
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.


Files:
Share it with friends: