OPaC programming style
 

Last updated on April the 30th 1997.

Back to OPaC bright ideas !

Function arguments

In order to increase the readability of the sources, the following typedefs are defined for each type Typ by using one of the type definition macros (such as OP_TYPEDEF (Typ);) :

typedef Typ* TypPtr;		// pointer to type
typedef const Typ& TypIn;	// input argument - do not modify
typedef Typ& TypOut;		// output argument
typedef Typ& TypInOut;		// input and output argument

The In, Out and InOut types are used for passing objects, structures and other complex data types. Numbers also use these type definitions, but the In suffix is often being dropped, since it is not useful to transmit small amounts of data through reference rather than through copy.

The following function definitions are therefore equivalent :

Bool FindCell (OPListIn list,
               Count index,
               OPCoordOut width,
               OPCellOut cell);


Bool FindCell (const OPListRef& list,
               Count index,
               OPCoord& width,
               OPCellRef& cell);

Bool is the fundamental boolean data type (either FALSE or TRUE). OPList and OPCell are both object classes (this is a special case, see below), OPCoord is a co-ordinate specification (usually a real number) and Count is a simple unsigned 32-bit integer.

Objects used as arguments to functions (such as OPList) or methods are in fact transmitted through the use of smart pointers; for class Typ, the following equivalencies are used :

typedef const TypRef& TypIn;	// input argument - do not modify
typedef TypRef& TypOut;		// output argument
typedef TypRef& TypInOut;	// input and output argument

Smart pointers and OPObjectRef

OPaC sources never use pointers to objects : this helps reducing the probability of errors related to erroneous pointer manipulation, null-pointer crashes, memory leakage, etc. Objects are accessed exclusively through this or smart pointers.

Smart pointers (also improperly called references in the OPaC jargon) encapsulate the real object pointers. They provide dynamic type checking, validity checking before method invocation (calls to invalid objects are trapped automatically) and automatic reference counting (which can be used to free objects when they are no longer used).

The smart pointers are all derived from the base class OPObjectRef, which provides the following public methods :

The C++ operator -> has been overloaded in order to provide an access to the encapsulated pointer. Thus, the following statements are equivalent :

// Using normal pointers :

OPView* view = GetMyView ();
view->Draw ();


// Using smart pointers :

OPViewRef view = GetMyView ();
view->Draw ();	

The overhead introduced by the use of smart pointers is rather small but not totally insignificant : operator -> has been defined inline and its validity checking only requires a simple if (object != 0) statement. Modern compilers generate about three to five extra CPU instructions (note that this is architecture dependent).

Object allocation and creation

OPaC objects cannot be instanciated as local or static variables. They can only be instanciated dynamically through the macro OP_NEW and its variations, the replacement for operator new. To ensure that objects are not created locally by accidental, the constructors of all classes are declared as protected. This also removes the risk of using pointers to objects, since they cannot be allocated.

OP_NEW (OPView); returns a smart pointer of type OPViewRef, which points to an instance of class OPView. The allocation is based on the default OPaC memory zone and is performed by the OPaC class manager, implemented by OPClassMan.

No arguments can be transmitted to the object on its construction. This is not considered to be an important restriction, since the constructor should only be used for minimal object initialisation; further initialisation — requiring parameters — will need dedicated methods.

Object destruction

OPaC objects cannot be deleted explicitly : the memory management is based on automatic destruction and garbage collection. If an object is no longer needed by the application, it will be automatically deleted.

A programmer might still want to delete an object explicitly. A special method Delete exists for this purpose : it prepares the object for its destruction but does not release the memory yet. The object's memory will be freed only after the destructor has been called.

The following example compares traditional object allocation and destruction with the model used by OPaC :

OPView* view = new OPView ();
...
delete view;
view = 0;


OPViewRef view = OP_NEW (OPView);
...
/* no explicit deletion needed */
view = 0;

Arrays of objects

Native C++ arrays are not recommended, since they do not provide any runtime boundary verification. Furthermore, C++ arrays are restricted to a single data type. OPaC provides an OPArray class which implements the same basic functionality as the native arrays of smart pointers, plus dynamic re-sizing, runtime type checking, boundary verification and automatic cleanup on destruction.

The following example compares native arrays of references with OPaC arrays :

OPViewRef array[3];
array[0] = GetMyView ();
array[0]->Draw ();
num_elem = sizeof (array) / sizeof (array[0]);
/* resizing is not possible */


OPViewArray array (3);
array[0] = GetMyView ();
array[0]->Draw ();
num_elem = array->GetSize ();
array->SetSize (5);

The OPViewArray is a subclass of an OPArrayRef which implements operator [] returning values of type OPViewRef&. The size of the array can be specified at the construction time (note that parenthesises (3) are used instead of brackets [3]) but can also be changed afterwards by calling SetSize.

Defining a class

OPaC needs extensive runtime information about the existing classes in order to achieve dynamic message passing and automatic class field browsing. Furthermore, a smart pointer class has to be defined for each class.

Macros for class definition

The necessary information is provided by a set of macros :

Both OP_EXPORT and OP_DEFINE_ARRAY should belong into a common header file, for a given project. This helps reducing recompilation time whenever a class has been modified. Class definitions often refer to other classes only through their references, so there is no need to include the full class definition. This policy is used extensively by OPaC and most class references are defined in file OPaC/export.h. A class referring to another one, for example OPViewRef, will not need to include the specific header file (in this case OPaC/view.h) to use the references in the header file. Only the implementation file will need to include it (OPaC/view.h) if it calls its methods.

The following example demonstrates the use of the macros that have just been presented :

// Definition of the reference class OPTestRef and of the array
// type OPTestArray (and associated OPTestIn, OPTestOut, etc.)

OP_EXPORT (OPTest, OPObject);
OP_DEFINE_ARRAY (OPTest, OPObject);

/*
 * The class OPTest demonstrates how a simple class can be derived from
 * the OPaC base class.
 */

class OP_PUBLIC OPTest : public OPObject
{
protected:
    OPStringRef user_name;

protected:
    OPTest ();
    virtual ~ OPTest ();
    virtual void StoreInstance (OPStorageIn storage);   /* OVERRIDE */
    virtual void RestoreInstance (OPStorageIn storage); /* OVERRIDE */

public:
    virtual Bool SetUserName (OPStringIn name);
    virtual Bool GetUserName (OPStringOut name) const;
    virtual void SayHello ();

    OP_DEFINE_CLASS (OPTest, OPObject);
};

The example defines both the constructor and the destructor as protected. This is required by OPaC so that no automatic object can ever be created on the stack. The only allocation mechanism supported by OPaC is provided by the class manager (which uses a special construction function provided by the class definition macros).

Most objects which have associated data provide methods to store and restore their state, namely StoreInstance and RestoreInstance. Each class should call its super-class methods before doing its own work. Storing and restoring is as simple as using the traditional C++ iostream operators << and >>.

Macros for class implementation

The macros used for the class definition do not generate everything that is needed by OPaC for its run-time system. An additional macro needs to be used in the implementation file in order to provide the trampoline functions and static variables needed by OPaC. Depending on the class type, one of the following macros has to be used :

An implementation file typically looks like the following listing :

/*
 * test.h
 *
 * OPaC test class used as a "hello world" sample.
 */

#include <OPaC/test.h>

OP_IMPLEMENTATION (test.h);
OP_IMPLEMENT_CLASS (OPTest);

OPTest::OPTest ()
{
    ...
}

Macros for dynamic message passing

In order to make dynamic message passing possible, a set of macros has to be used to define the target methods and their arguments.

A generic dynamic method definition needs quite a few macros to define its interface :

An actor is a very simple, argument-less, target. An observer is a special target which takes an object of type OPObjectIn and an ID of type Card32 as arguments. They are defined using the following macros :