Class Basics
Divider

Classes are a very important part of QDL, and although you don't actually have to make them, virtually all programs will. I'll be talking about Classes so much in this section that I'm not going to bother with the bolding and capitalization of the word.

In its broadest sense, a class includes almost any QDL data type. The only data types that are not classes are:

Classes fall under the following categories:

When most people refer to classes, they are thinking of the last bulletted item, or perhaps the last two.

A data type qualifies as a class if it has at least one constructor, and exactly one destructor. Neither the constructor nor the destructor have to do anything, but both must be present and have an actual address in code space. Even the built-in types such as Integer have constructors and a destructor.

Note: A derived class does not automatically receive a constructor unless there are multiple base classes, all of which have a default constructor.  A derived class does not automatically receive a destructor unless there are multiple base classes.

If a class needs a compiler-provided constructor or destructor, its only job is to call the constructors/destructors for the base classes.  If there is only one base class, no constructor or destructor is necessary as the compiler can simply call the base class's constructor or destructor directly.  Whether the compiler provides a default constructor for derived classes is insignificant.  That a default destructor is provided becomes significant only when the base class's destructor is Final, or the class is Stripped: the rule is that the derived class cannot have a destructor, not even a default one.

In QDL, you can define your own classes, which are made up of:

Classes may also have, declared within them:

An instance of a class is a data structure that contains:

The layout of these things in the structure is implementation-defined.

The code (including declarations) of all built-in non-reference classes is "known" already when compilation starts and does not have to be defined. Built-in classes can be redeclared, excluding

How the compiler treats these redeclarations is implementation-defined, but the class's behaviour should not change provided that the class is redeclared correctly.

Reference types for all classes exist without the reference type class being defined; indeed, to attempt to explicitly declare a reference type is illegal.

Class Scope

A class has two scopes associated with it: a static scope and a nonstatic scope. Members that are declared as Static are in the static scope, and members that are not declared as Static are in the non-static scope. Where a bit of code has access to the non-static scope, the static scope can also be accessed; the reverse may not be true.

Functions that are part of the class have implicit access to other members of the class: that is, the members of the class are in the function's scope. However, Static member Functions are only within the class's static scope.

Only one copy exists of Static member variables. Meanwhile, a copy of each non-static member variable is located in each instance of the class, but there is no global copy. A member function can also be Static, which means that it is not called on a particular instance of the class. In fact, no instances of a class need exist to call a Static member function. A Static member function does not have a This variable and therefore cannot access non-Static member functions or variables directly.

Syntax

A class is declared using the Class statement, which can be located within functions, at global scope, or inside another class. Its syntax is:

class-statement: class-modifiers Class class-resolved-typeidentifier : class-clauses (access-label | class-member)* End Class;
class-modifiers: [Final | Stripped | Exported | Abstract | Published]*
class-clauses
: (renames-clause* | ( inherits-clause | tags-clause | uses-clause )* )
class-member: class-statement | var-statement | function-declaration-statement | function-statement | enum-statement
access-label
: ( Public: | Private [access-list-spec]: | Protected [access-list-spec]: )

When a member is a Class statement, the scope of the type identifier(s) created by the Class statement is the static scope of the enclosing class.

Before the keyword Class is one or more modifiers. There can be more than one modifier, but the same modifier may not be listed more than once. After the class is "opened" with a colon, there are zero or more class-clauses, which specify additional parameters for the class.  The modifiers are described in this section, and the clauses in this section.

Implement Statement

implement-statement: Implement [class-resolved-typeidentifier]: (var-statement | implement-statement | function-statement)* End Implement;

This statement tells the compiler that all variable declarations and function definitions inside are part of the class specified by class-resolved-typeidentifier. For example, the following:

Implement MyClass:
Static MyInt: Integer;

Function MyFunc () Returns Integer:
	Return MyInt * 2;
End;
End Implement;

is equivalent to:

Static MyClass::MyInt: Integer;

Function MyClass::MyFunc () Returns Integer:
	Return MyInt * 2;
End;

Implement can only implement classes in the current Namespace.

Functions in Classes

When a function is declared inside a Class statement, it becomes a member of the class. The body of the function has access to the members of the class as if they were global variables (i.e. no qualification is required to access the members.) It is possible to define the function body inside the Class statement, like this:

Class MyClass:
	X: Integer;
	Function SquareMe ():
		X = X * X;
	End;
End Class;

However, this not usually done except for very small functions. When you do it, the compiler assumes that the function should be Inline. More often, you will define the function outside the Class statement. To do this, you must somehow indicate to the compiler that the function you are defining belongs to the class. There are two methods to do this; the first is to use scope resolution in the function header:

Function MyClass::SquareMe ():
	X = X * X;
End;

This method requires you to insert the name of the class and :: before the name of every function. To avoid this, you can use the second method, the Implement statement.

This

When writing a non-static function in a class, you have implicit access to all the members of the class. Sometimes, though, you need access to the class itself - perhaps to use an operator function, or to use the class as a parameter to some function. In this case, you can use the This keyword. The This variable is available to all non-static member functions of a class. The pointer to the class contained in This is passed by the caller on the stack as a hidden parameter.

Access Modes

Every member of a class has an access mode. By default, members of a class have the Public mode, but the mode can be changed using one of these access labels, which change the access mode of the members that follow it.

access-list-spec: <[Inherits] resolved-typeidentifier>
Note: the construct Template template-identifier:, shown in the syntax for Class above, is not an access mode. It is only valid in Template classes, and is described in The Template Modifier section.

These modes control access to the subsequently listed members of the class as follows:

Public

Any code can access the members.

You might be wondering why the default access mode is Public, especially when the default in C++ is private. The logic goes like this: it's sensible to have the public interface for a class listed first, since that is typically the first part of the class a client programmer looks at.  Private and protected sections are unimportant to a client, so they should be out of the way at the bottom.  The default encourages this.  It also facilitates top-down programming, an arguably more natural way to develop a problem than bottom-up programming.

Private

Code for members of the class can access the member. Derived classes do not have access to the member except as overridden by access-list-spec.

In both the Private and Protected modes, an instance of a class has access to Private and Protected members of other instances of the class.

A section of a class is "completely" private only if no other classes have access to it.  Thus, if the access-list-spec is missing or empty, the section is completely private.  All Functions in a completely private section are implicitly Final.

Protected

Code for members of the class, and code for members of derived classes, have access to the member.  This is equivalent to using Private with Inherits A in the access-list-spec, where A is the name of the class being defined.

The access-list-spec

This optional section of the statement specifies one or more other classes that are also allowed to access this section of the class.  Each access-spec on the list is simply the name of another class (with a resolved scope, if necessary), optionally preceded with the keyword InheritsInherits specifies that all derived classes also have access to the section that follows, not just the specific class specified.

The Point-of-view

Every Class, Function and Implement block has a point-of-view.  The point-of-view determines whether any given member function, member variable, or inner class (which will be called a thingie in this paragraph) is visible or invisible to the code in the block.  If a thingie is visible, it can be used; if it is invisible, it cannot be used.  Visibility can resolve name collisions; if one thingie collides with one or more other thingies, the collision is resolved automatically if only one thingie is visible.  (If multiple thingies are visible, scope resolution must be used.)

For an example of how point-of-view works, take a look at the following:

 

Run-time Type Information (RTTI)

Run-time type information, generally shortened to RTTI in this documentation, is an important part of QDL.  It makes possible many things that were impossible in C and C++.  The uses of RTTI include:

In traditional data structures, there is no RTTI.  For example, if you make a class in C++ such as this:

class X {
	int x, y;
	void f() { cout << "X::f"; }
};

Instances of the class are stored exactly as they appear in the declaration:

Byte# 0 1 2 3 4 5 6 7
  int x int y

There is no information about the class stored with static storage class and there is no vtable pointer.

If you make the equivalent class in QDL, however:

Class X {
	Var X, Y: Integer;
	Function F() { StdOut.Print "X::F"; }
}

You get considerably more than you asked for.  For starters, the structure probably looks like this:

Byte# 0 1 2 3 4 5 6 7 8 9 10 11
  RTTI pointer X: Integer Y: Integer

The RTTI pointer is at the heart of all QDL's RTTI capabilities.  It is this pointer that allows you to:

The pointer points to an implementation-defined static RTTI data structure that contains the information that helps make these capabilities possible. 

Where a compiler places RTTI pointers in the structure of a class instance, how many to use, what exactly those pointers point to and the precise structure of the static RTTI is implementation-defined.

Derivation (inheritance) and overriding

A class can be derived from one or more other classes using the Inherits clause of the Class statement.  The inherited classes may also inherit other classes, and so on.  For example:

Class A:
	Function F:
		StdOut.Print "A::F";
	End;
	X: Integer;
End Class;

Class B:
	Inherits A;
	Override Function F:
		StdOut.Print "B::F";
	End;
End Class;

In this case, B is a derivative of A.  It gets the variable X and the function F from A, and overrides F.  More on inheritance can be found in the documentation for Inherits.  Classes derived from a given class are also known as that class's subclasses, and a given class's base classes are also known as its superclasses.

Overriding a function means that you're making a function with the same signature as a function in the base class.  Overriding is part of a fundamental OO concept called polymorphism.  When you override a function, you're not simply creating a new function that happens to have the same name and signature as a function in your base class.  Instead, you are creating a new behaviour for the existing function.  To illustrate how this works, take a look at the following:  

Function Main:
	B: B;
	A: @A (B);
	A.F;
End;

Here we have a variable of type B, and another variable that is a reference to A.  According to the laws of inheritance, you can put the pointer to a derived class in a reference to the base class.  That's what was done here: A was assigned the reference to B.  When A.F is called, the output is B::F. The question is, how could the compiler call B::F instead of A::F?  After all, it was a reference to A, so wouldn't it make sense for A::F to be called?

In OO land, a land in which QDL is a faithful, law-abiding citizen, this is not the case.  In fact, B::F should be called because A actually references a B.  How does the compiler know that A refers to a B?  Is it just that the compiler is very observant and noticed that it was set to reference B?  Probably not.  This is where polymorphism comes into play.  In traditional procedural languages, functions are always called directly (except when calling through a function pointer, but, er, that's irrelevant to the discussion at hand.)  The compiler determines at compile-time what function to call, and calls it.  This is called early binding.  However, polymorphic functions work differently.  By default, all non-Static QDL functions are polymorphic.  When you want to call a polymorphic function through a reference variable, the compiler goes through at least one extra step to determine what function to call.  The exact function to call (in this example, A::F or B::F) is usually unknown at compile-time, so one or more extra steps are taken at run-time to determine the function to call.  This is called late binding, and it typically works by having a secret pointer stored with each class instance that points to a table of function addresses.  When you call a function in code, the address of the function is looked up from this table.

In QDL, this function table, often called the vtable, is part of the run-time type information (RTTI).

When creating a function with the same name as a function in a base class, it does not have to have the same signature.  If it does not, the new function coexists with the old function; the compiler uses decides which function to call automatically, just as with regular overloaded functions.  For example:

Class C:
	Function F;
End Class;
Class D:
	Function F ("Different Signature");
End Class;

In this case, D::F does not override C::F, it just overloads it.  Now, when you attempt to call F in code, to compiler resolves which function to call the same way it does for overloaded functions:

Function Test:
	D: D;
	D.F;                     // Calls C::F
	D.F Different Signature; // Calls D::F
	C: @C (D);
	C.F;                     // Calls C::F
End Function;

Versioning, New, and Override

Originally, I'd introduced the Override keyword to make overriding explicit--both for code documentation purposes, and to make sure the programmer knew what (s)he was doing:

then the compiler would issue an error.  I was not thinking about versioning when I introduced Override.

For C#, Microsoft does something similar for their source-level versioning system.   It is different from my original system in two ways:

  1. In addition to override, Microsoft provides new, which does the opposite: instead of overriding the base class function, it hides it.  In other words, the two functions are made completely separate so polymorphism does not apply.  The same thing can be achieved in QDL by using the Hide pseudo-statement in the Inherits clause; however, new has two advantages:
  2. When an appropriate keyword is not present, the C# compiler produces a warning instead of an error.

Point (2) is most important for versioning.

The logic goes like this: often, the base class and derived class are developed independently by two different parties, both of whom extend the classes independently.   Obviously, it is desirable for the program to be able to compile with a new version of the base class.  Most often, the base class developer will keep the existing members, but add others.

Suppose that a function F is added to the derived class D, but later, an F is added to the base class B:

Class B:
	...
	Function F();
	...
End Class;
Class D:
	Inherits B;
	...
	Function F();
	...
End Class;

In some languages, this will result in overriding, and in others, it will result in hiding.  In C++, D::F will override B::F if B::F is virtual, but hide B::F if B::F is not virtual.  Note that in C++ and Java, there will be no warning about this unforseen "conflict"; thus, the derived class programmer may not realize that this name clash has occurred and thus will not consider the implications thereof.  I'm not sure if this is actually a serious problem from a practical standpoint, but certainly from a philosophical standpoint, the coder really ought to decide the relationship of D::F to B::F and change D accordingly.  An error would force the coder to do this, but at the same time it is advantageous for the code to be able to compile without changes: after all, if possible, new versions of B made by the independent developer should not break D or the code that uses D.

In general, a function in a derived class should override a function in a base class only if the derived class function follows the contract of the base class function.   Microsoft feels this is unlikely in a case like this, since B::F did not even exist when D::F was implemented.  I would suggest it more likely than Microsoft thinks: after all, the functions were given the same name (and signature, at least in the example provided by Microsoft), so the two developers probably had similar, if not identical, purposes and contracts in mind for the two functions.   Following Microsoft's reasoning, the default behaviour for this case in C# is to ussue a warning and make D::F a new function, thus hiding B::F.   Personally, I don't know whether overriding or hiding is the better default behaviour, but I do see the advantage of issuing a warning rather than an error; in this case, the compiler must choose a default behaviour.  In QDL, therefore, the compiler defaults to hiding the base class member.

Whether to add a New modifier to complement Override was a tough decision. On the one hand, well, I sure like orthogonality!  And it's nice to have an obvious indication in the derived class that there is a same-name function in the base class.  On the other hand, there is already a more general feature for name hiding, the Hide pseudo-statement in the Inherits clause, and I don't like to duplicate features like this.  Also, it seems to me that if there were a New modifier, the programmer should be allowed to specify it and hide the base class function with Hide.   However, this would be counterintuitive to the compiler: Hide makes the original function invisible to the derived class, so New would no longer be applicable.  For these reasons I've decided against adding a New keyword, and simply suggest using Hide instead.

In a nutshell, you should apply the Override modifier on a derived class function when you want to polymorphically override a base class function, and use Hide in the Inherits clause to hide the base class function if it is unrelated; if you fail to use either keyword, the compiler will issue a warning and continue compiling as though you'd hidden the base class function.  However, I would strongly recommend that you avoid using Hide for this purpose, because having semantically unrelated functions with the same name and signature is downright confusing!

If Override is applied to a function which does not have a counterpart in a base class, the compiler issues a warning.

If a base class function is renamed, it can be overridden with the new name:

Class B:
	Function F();
	Function G();
End Class;
Class D:
	Inherits B {
		Rename F as Q; // B::F is known as Q within D
		Hide G;
	};
	Override Function Q(); // D::Q overrides B::F
	Function G();
End Class;

The above example also demonstrates hiding a base class function to make way for a separate derived class function.  If the line Hide G; was not present, the compiler would issue a warning about the name conflict and continue compiling as if B::G had been hidden.  Note that you can declare D::G even if B::G is Final; Final disallows polymorphic inheritance, but not name re-use.  Also note that, in many cases, you may find it more useful to rename the base class function, rather than actually hiding it.

Access modes and overriding

If a function is invisible (private) from the point of view of the derived class, the derived class cannot override it.  For example:

Class E:
Private:
	Function F;
End Class;

Class F:
	Inherits E;
	Function F;
End Class;

In this case, E::F is invisible from the point of view of F.  Therefore, F::F does not and cannot override E::F.  Using the Override keyword in this circumstance would be invalid. E::F and F::F are completely separate and will have different vtable entries.  Whether or not E::F is declared Final is irrelevant to F.

Constructors

The constructor of a class is a special function that is called automatically when an instance of a class is created. The constructor gives a class the chance to initialize itself. A constructor function has the name Constructor; for example,

Class MyClass {
	Function Constructor();

The Function keyword is optional when declaring Constructor functions.

A constructor may also have arguments. The constructor with arguments can be called by specifying those arguments in a Var declaration. For example:

Class MyClass {
	Function Constructor (X: Integer) Do
		MemberVar = (X: String, Hex);
	End;
	MemberVar: String;
}
Function Main()
{
	MyInstance: MyClass(20);
	StdOut.Print MyInstance.MemberVar;
}

Output: 0x14

Constructors may not be declared as Abstract.  There is a special rule with regard to overriding constructors: you can't.  You never need to use the Override keyword on a constructor; indeed, it is not allowed and will result in an error.  When an object is constructed, the constructor is not called polymorphically, since the exact (most-derived) type is known at compile-time.  Furthermore, you cannot explicitly call the constructor for a base class while instantiating an object.  Therefore, the Override keyword, which has to do with polymorphism, seems inappropriate considering the way constructors are normally used.

The Static Constructor

A class may provide a Static constructor. This constructor cannot take any arguments, or have delimiters. A class can have both a Static and a non-Static constructor that takes no arguments. This is an illegal condition for functions that are not constructors or destructors.

The static constructor is called to allow the initialization of static members of a class. It is called before Main() is executed.  It can be ensured that a static constructor is called after static constructors for other classes by having a Uses clause. 

Default Constructors

As with C++, the programmer does not have to provide a constructor; the compiler will implicitly add one if there is none in the class declaration. If a constructor or destructor is added by the compiler, it is called a "default" constructor or destructor. A default constructor/destructor is public, non-virtual, and inline. All it does is call the base class constructor(s) and member variable constructors (if any).

A default constructor takes no arguments. If the programmer provides a constructor with arguments, the compiler will not provide a default constructor. This means that, when instantiating the class, arguments must be provided. For example:

Class TheClass:
	Function Constructor (Arg: Integer);
	...
End Class;

Var TheInstance: TheClass; // Error!  Arguments MUST be provided
Var TheInstance: TheClass (0); // OK

The compiler may optimize by having a single empty function to represent all empty constructors. (especially when it comes to things like simple integers.) It should be noted, however, that a constructor that appears to be empty may actually contain code to call base class constructors and member constructors.

Destructors

A destructor is a special function that is automatically called right before an object is destroyed. It gives an object the chance to free memory and other last-minute cleanup tasks. A destructor function has the name Destructor; for example,

Class MyClass:
	Function Destructor();

The Function keyword is optional when declaring Destructor functions.

A destructor cannot have any arguments or delimiters, nor can it return a value.

The destructor is called automatically before an object is destroyed.  Every destructor function automatically contains code to call the destructors for base classes.

The Override keyword is not allowed when declaring a destructor.  The reasoning here is that you are not overriding the base class destructors as much as you are simply adding another function at the head of the chain that is called in the process of destructing an object.

The Throw statement should generally not be used within a destructor.  If an exception is allowed to escape from a destructor, the program's resultant behaviour is undefined.  If there is any possibility that an exception might be thrown in a destructor, you should be sure you Catch the exception before exiting the function.

The Static Destructor

A class may provide a Static destructor. This destructor cannot take any arguments, or have delimiters. A class can have both a Static and a non-Static destructor.

The static destructor is called after Main() ends.  The static destructors are called in the reverse order that the static constructors were called.

Table of Contents Qwertie's Site/Mirror
Next
Previous
Hosted by www.Geocities.ws

1