Mr. Cluey : Special Topics : Constructors


Implementing Constructors in 4Test

Alain Lepalme, back in QAPUSER digest #808
> I've been trying to find out if there is an 
> elegant way of defining constructors for user 
> defined classes.

Hmm - the answer to that is somewhat dependent on what you need from your constructors.

In C++, you can't initialize member variables in the class definition, so this task is moved to the body of the constructor or to the initializer list of the contructor. In 4Test, winclass definitions can include member initialization, so that part of the job can be handled very elegantly.

   winclass MyClass
   {
      long nNumber = 5 ;
   }

4Test also allows the use of functions as initializers; in particular, it allows you to use member functions. So you can also do this

   winclass MyClass
   {
      long nNumber = MyNumber () ;

      long MyNumber() { return 5; }
   }

This gives us an elegant solution if our requirements can be met by a *default* constructor (a constructor that takes no arguments). Simply create an Init () method to handle any activities that straight forward initialization cannot handle.

   winclass BaseClass
   {
      boolean bInit = Init () ;

      boolean Init ()
      {
         // initialization goes here

         return true ;
      }
   }

In 4Test, all class methods are virtual, so each class derived from BaseClass can declare its own Init() method. Of course, derived classes should explicitly call the Init of the immediate parent class. To ensure that the correct (dynamically linked) version of Init() is called, we use the this pointer

   winclass BaseClass
   {
      boolean bInit = this.Init () ; /* see note [1] */

      boolean Init ()
      {
         // BaseClass initialization goes here

         return true ;
      }
   }

   winclass ChildClass : BaseClass
   {
      boolean Init ()
      {
         derived::Init () ;

         //ChildClass initialization goes here

         return true ;
	}
   }

Elegance decays exponentially if we need arguments with our constructors. The fundamental problem is the constuctor for the window class. I don't know what the constructor for the window class actually looks like, but its behavior strongly suggests that it is something like

   class window
   {
   public:
      ( Interface *pInterface, const char * sTag = "" ) ;
   }

in other words, the 4Test statement

   window MyWin = MyClass( "MyApp" ) ;

might be written in C++ as

   window MyWin( &MyClass, "MyApp" ) ;

What does this mean? Well, given that we can't change the behavior of the window constructor, we have to make do with what it gives us for arguments - in other words, a string.

As far as I can tell, a generic winclass cannot get at the string passed to the window constructor, which is inconvenient to say the least.[2] However, if your base class is derived from AnyWin, then you will find the string carefully tucked away in the WndTag member, prefixed by "/". SubStr() will allow you to recover the original string, which can then be used as your constructor argument.

So, we have a relatively elegant constructor if all we need is a single string argument. If we need, for instance, a number, we can use Val to get the number from the string. If you need to pass a complicated argument (which cannot be easily cast to a string - for instance a record), or multiple arguments, there are still options available.

The first is to write a function which takes the arguments you need to pass and embeds them in the string. For instance, if you wanted a constructor with three int arguments, you might pass in "1,2,3" then, inside of Init(), split the string on the ','.

A second is two write the constructor arguments to a temporary file, and use the string to hold the path to that file. Inside the contructor, you can load the values you just stored, and begin constructing the object.

A third is to take advantage of the reference operator ("@"), passing in the name of a variable that holds the data. Unfortunately, the variable that you name must be visible from the scope of the class definition - that is to say, globally.

Obviously, with all of these, type safety goes right out the window.

Should you go this route, it is important to remember that only the declared class should be interpreting the constructor arguments (although you would use base class methods to decode them). To ensure that classes do the right thing, you would split the interpretation of the arguments from the initialization

   /* note: BaseClass derived from AnyWin to give access to the WndTag member */
   winclass MyClass : BaseClass
   {
      boolean bInit = this.Ctor () ;

      boolean Ctor ()
      {
         /* Extract the constructor arguments, using one of the methods above */
         /* for this example, A, B, and C */

	   return this.Init(A,B,C) ;
      }

      boolean Init ( string A, string B, string C )
      {
         derived::Init( A, B) ;

         /* initialize this using A,B, and C */

         return true ;
      }
   }

I sure do hope this helps answer the question.

[1] I believe this works, but haven't actually checked it - my QAP license doesn't reach my home computer. The this pointer definitely works inside of member functions, but I don't know that it works in the initializer list. If it doesn't, you can hack around it with

   winclass BaseClass
   {
      boolean bInit = _Init () ;

      boolean _Init () { return this.Init() ; }

      boolean Init () { /* as above */ }

   }

[2] It might be possible if the winclass in question is defined with the correct members. I haven't tried this experiment myself.


Mr. Cluey : Special Topics : Constructors

Return to ATS Automated Testing Resources Page