next up previous contents
Next: 5 Templates Up: IMPL_repdoc Previous: 3 Shallow and Deep   Contents

Subsections


4 Inheritance

Inheritance is the ability to create a derived class that includes the properties of one or more other superclasses or base classes. One of the most common uses of inheritance is to define a set of different specialized classes from a more general class.

There are two important benefits to inheritance, the first being the ability for a derived class to utilize the functionality of the base class, the second being polymorphic behavior, or the ability for a base class to be used as an interface for all derived classes.

This first benefit of inheritance can be emulated in C and F90 simply through class containment. In the derived class we include an object of the base type and provide an interface by which the user of the object may call those functions. This emulation requires more typing and extra constructs in C and F90 than C++ but the functionality is entirely possible.

The second benefit, polymorphism, is the ability for a class to take on different behavior depending on it's type. A typical example is the window->drawPixel routine, which allows an algorithm to create a window object and draw pixels without worrying what graphics library is beneath the hood (X11, openGL, Win32, etc...). To implement this behavior you must have the concept of a base class pointer, which can point to any class in the inheritance hierarchy. The base class defines virtual functions which act as an interface for all derived types of the class. When a virtual function is called from a base pointer, the runtime environment figures out what the true type of the object is and dispatches the call to the appropriate derived function. This is an extremely powerful capability that allows us to write complex algorithms using a base object which operates on a whole set of derived types.

Polymorphic behavior is often implemented in C using the void* and an enumerated class-type field. Since C supports function pointers, a virtual function table may be constructed, if desired. F90, on the other hand, does not allow this kind of ``under the hood'' manipulation that the void* provides, and makes it far more difficult to implement polymorphism. The biggest stumbling block is that a base pointer is not really possible (we lack a casting operator), and we end up implementing the base class as something more like a union of derived classes. This is developed further in the example.

This implementation of inheritance in F90 is developed at length in [3].

F90 does, however, offer a means of supporting polymorphism through the creation of generic functions. A generic procedure (that is, a shared procedure name) can be created for an assortment of specific procedures through the interface keyword. The specific procedure is resolved by matching the argument list in the call to the arguments of the procedures that share the interface. Thus an interface that appears to support polymorphism though inheritance can be created using F90. This straightforward implementation of polymorphism in F90 does not perform run-time dispatching. However, run-time polymorphism can be supported in F90 through the creation of a polymorphic class (see [4]).

4.1 C++ Implementation

Code for the example is in inheritance_example/C++.

This example illustrates how an F90 interface can be affixed to a C++ inheritance hierarchy.

The classes:

#include <iostream.h>
#include "ftn.h"
class climber {
public:
  virtual ~climber() {}
  void speak() {
    cout << "I climb ";
    speak_specific();
  }
  virtual void speak_specific() {}
};
class alpine : public climber {
public:
  void speak_specific() {
    cout << " snowy couloirs" << endl;
  }
};
class rock : public climber {
public:
  void speak_specific() {
    cout << " rock faces" << endl;
  }
};

The F90 interface module:

	module climberMod
	
	type climberPtr
	sequence
	integer(r8) this
	end type

	contains

	function alpineCreate()
	type(climberPtr) :: alpineCreate
	call c_alpine_create(alpineCreate)
	end function
	
	function rockCreate()
	type(climberPtr) :: rockCreate
	call c_rock_create(rockCreate)
	end function
	
	subroutine climberSpeak(this)
	type(climberPtr), intent(in) :: this
	call c_climber_speak(this)
	end subroutine
	
	subroutine climberDestroy(this)
	type(climberPtr), intent(in) :: this
	call c_climber_destroy(this)
	end subroutine 

	end

The F90 type definition in this example differs significantly from that in the shallow example in that the C++ and F90 types are no longer associated with similar structures in memory. The F90 type in this example is now merely a handle with enough memory to store the C++ this pointer.

The other significant difference is that the memory now is allocated dynamically on the C++ side (C++ must be allowed to populate the virtual function tables), not the F90 side.

The C ``glue'' code (same as lightweight example except for the inclusion of an allocation and deallocation routine and an additional layer of pointer indirection):

extern "C" {
  
  void FTN(c_alpine_create)(climber **ths) {
    climber *ap = new alpine();
    *ths = ap;
  }
  void FTN(c_rock_create)(climber **ths) {
    climber *ap = new rock();
    *ths =  ap;
  }
  void FTN(c_climber_speak)(climber **ap) {
    (*ap)->speak();
  }
  void FTN(c_climber_destroy)(climber **ap) {
    delete (*ap);
  }
}

An example F90 driver:

	program climberEx
	Use climberMod	
	type(climberPtr) :: climber1, climber2

	climber1 = alpineCreate()
	climber2 = rockCreate()
	call climberSpeak(climber1)
	call climberSpeak(climber2)
	call climberDestroy(climber1)
	call climberDestroy(climber2)

In this example the F90 driver creates several different subclasses of climber. The F90 program is able to use an identical interface to each class (the one provided via virtual functions in C++). Hence, F90 gets all of the benefits of the C++ inheritance hierarchy. This example actually extends the capabilities of F90.

The F90 program stores a pointer to the class, and within the memory image of that class is all the information about what type the class is and how to dispatch the correct virtual functions for that class.

The C interface functions accept the address of the F90 type, in which is stored the address of a class. Hence, the C function recieves a pointer to pointer:

  void FTN(c_climber_speak)(climber **ap)
  {
    (*ap)->speak();
  }

4.2 F90 Implementation

Code for the example is in inheritance_example/F90.

Implementing an inheritance scheme is possible in F90, though it is cumbersome. This example demonstrates one of the methods that might be used to implement inheritance if F90. It is one of the models that Szymanski and Decyk [3] use:

	module climberMod
	integer, parameter :: ALPINE = 1,
     &	ROCK = 2

        type alpinePtr
	sequence
	integer :: some_alpine_data
	end type

        type rockPtr
	sequence
	integer :: some_rock_data
	end type

	type climberPtr
	sequence
	real(8) :: climber_data
	integer :: type
	type(alpinePtr), POINTER :: alpine
	type(rockPtr), POINTER :: rock
	end type
	
	contains

	subroutine alpineInit(alpineCreate)
	type(climberPtr) :: alpineCreate
	allocate(alpineCreate%alpine)	
	alpineCreate%type = ALPINE
	end subroutine
	
	subroutine rockInit(rockCreate)
	type(climberPtr) :: rockCreate
	allocate(rockCreate%rock)	
	rockCreate%type = ROCK
	end subroutine
	
	subroutine climberSpeak(this)
	type(climberPtr) :: this
	print *, "I climb "
	if (this%type .eq. ALPINE) then
	   call alpineSpeak(this)
	else if (this%type .eq. ROCK) then
	   call rockSpeak(this)
	endif
 
	end subroutine 

	subroutine alpineSpeak(this)
	type(climberPtr), intent(in) :: this
	print *, " snow couliours"
	end subroutine

	subroutine rockSpeak(this)
	type(climberPtr), intent(in) :: this
	print *, " rock faces"
	end subroutine
	
	subroutine climberDestroy(this)
	type(climberPtr), intent(in) :: this
	if (this%type .eq. ALPINE) then
	   deallocate(this%alpine)
	else if (this%type .eq. ROCK) then
	   deallocate(this%rock)
	endif
	end subroutine 

	end

The example uses a union approach, where the object contains pointers to all the possible derived types. Only one of these pointers is allocated, and a type field is used to keep track of the derived class type. In virtual functions (climberSpeak), this type member is used as a switch to the correct function. This solution is entirely functional, although it wastes some space (the derived pointers), and it is more of a tabled approach, since adding a new class requires modifying the base object. This violates extensibility at least at the source level, though with care the programmer will not change any of the base class interfaces.

Wrapping the F90 module in C++ is similar to the reverse wrapping, however an extra layer of indirection (through F77) is necessary.

The F90 interface code:

        subroutine f_alpinecreate(this_arg)
        Use climberMod
        integer :: this_arg
        type(climberPtr), POINTER :: this 
        allocate(this)
        call alpineInit( this )
        call ESMF_loc(this_arg, this)
        end subroutine

        subroutine f_rockcreate(thisa)
        Use climberMod
        integer this_arg
        type(climberPtr), POINTER :: this 
        allocate(this)
        call rockInit(this)
        call ESMF_loc(this_arg, this)
        end subroutine

        subroutine f_climberspeak(this)
        Use climberMod
        type(climberPtr) :: this 
        call climberSpeak(this)
        end subroutine

We have created our own version of loc, since loc does not always return the address of the newly created type from the POINTER. Its behavior appears to vary from platform to platform.

Our version of loc, ESMF_loc:

void FTN(esmf_loc) (void ** return_arg, void *pointer_arg)
{
        *pointer_arg = return_arg;
}

The C++ wrapper class is below:

class climber;

extern "C" {
  void FTN(f_climberspeak)(climber *);
  void FTN(f_alpinecreate)(climber **);
  void FTN(f_rockcreate)(climber **);
}

class climber {
public:
  virtual ~climber() {}
  virtual void speak() {
    FTN(f_climberspeak)(handle);
  }
protected:
  climber *handle;
  int variable;
};

class alpine : public climber {
public:
  alpine() {
   FTN(f_alpinecreate)(&handle);
  }
};

class rock : public climber {
public:
  rock() {
    FTN(f_rockcreate)(&handle);
  }
};

This code stores the address of the FORTRAN type in the class variable handle. It passes this address back to the F90 functions. This is the same basic approach that was used in the opposite direction.

4.3 C Implementation

Inheritance from a superclass can be implemented in C by having the superclass struct be the first entry in a derived class struct (see, e.g. Rumbaugh). Thus all the attributes and operations of the superclass are included in the derived class. Since the superclass methods begin accessing the data objects they receive based on the pointer to the start of the struct, and they have no knowledge of memory in the struct beyond this, derived objects can be input into methods of either the superclass or the derived class.

Though this method cannot support multiple inheritance, it is an efficient way of reusing the same implementation of an operation for multiple classes. For example, a Vector class and a Matrix class that inherits from the Vector class can both use the same set of elementwise Vector operations. Typically an elementwise Matrix operation would be created corresponding to each elementwise Vector operation and assigned the function pointer from the Vector operation. Thus a programmer might pass a Matrix object into a Matrix_Clear method and not be aware that the operation was actually a Vector_Clear performed on the Vector embedded in the Matrix object.


next up previous contents
Next: 5 Templates Up: IMPL_repdoc Previous: 3 Shallow and Deep   Contents
esmf_support@list.woc.noaa.gov