Next: , Previous: Using Classes, Up: Top


5 Implementing Classes

Implementing a new class is relatively simple. You have to create three (or two if you prefer) source files, preferably with the class name. Then you can use the class implementation macros defined in ooc.h, and must define the predefined mandatory class methods.

5.1 Naming conventions

Although it is totally up to you, and has no effect on the operation of the ooc toolkit, I recommend using the following naming conventions:

5.2 Source files

The following files must be created for implementing class Foo that is a subclass (child) of Parent:

5.3 Class user header file

This file should be named as foo.h.


In foo.h you must decalare the class and its virtual functions, plus the public methods of the class.
You always have to use the virtual function definition block, even you class do not have any virtual function. In this case just leave this block empty.
     
     #ifndef FOO_H
     #define FOO_H 1
     
     #include "parent.h"
     
     DeclareClass( Foo, Parent );
     
     Virtuals( Foo, Parent )
                  
     EndOfVirtuals;
     
     /* Foo methods */
     
     Foo foo_new( int initial_value );
     
     int foo_get_value( void );
     
     #endif
     

Please note that there is no semicolon after the Virtuals.

5.4 Class implementation header file

The class implementation header file contains the definitions for data members of the class Foo. It is your choice if you creat a separate class implementation header, or you include this section in the foo.h as weel.


Including the implementation related definitions in the class user header file you make all class members public; in other word the user of class foo can access all data members simple via pointers.

Including the implementation related definition in a separeted class implementation header (e.g. called impl_foo.h) you make all data members protected; in other words the user of the class can not access it, but the subclasses always can.

Making really private members would be a bit complicated, and not supported by the macros. (See "pimpl" or "fast pimpl" idioms for a possible solution!)

The content of impl_foo.h should look like:
     
     #ifndef IMPLEMENT_FOO_H
     #define IMPLEMENT_FOO_H 1
     
     #include "impl_parent.h"
     #include "foo.h"
     
     ClassMembers( Foo, Parent )
     
         int    data;
         void * data_ptr;
     
     EndOfClassMembers;
     
     #endif
     

5.5 Class implementation file

In the class implementation file you must allocate the class description table and the virtual table of the class. Then you must implement the mandatory class member functions as below. After this mandatory section you can implement your class methods.


The class implementation file may be called e.g. foo.c, but it can consist multiple files if necessary, of course.

5.5.1 Class allocation

     
     #include "impl_foo.h"
     
     AllocateClass( Foo, Parent );
     

5.5.2 Class initialization

The most of the class properties are initialized in compilation time. However the vtable can not be initialized perfectly, so initializing a class means building up the class's virtual table.


You must initialize the virtual table only if your class defines new virtual functions; or you would like to override any virtual function of the parent class! If you don't have to do anything in the class initialization, just leave its body empty!

The mandatory function name for the class initialization function is the class name + the suffix of "_initialize".

This function has got a pointer to the class description table as parameter. You can access the class's virtual table via this pointer. The virtual table address is stored in the vtable field of the class description table, and the type of the virtual table is the class name concatenated with Vtable.

Example: overriding the parent's print virtual function:
     
     static
     void
     Foo_initialize( Class this )
     {
         FooVtable virtuals = (FooVtable) this->vtable;
     
         virtuals->Parent.print = virtual_foo_print;
     }
     


Example: aquiring some global resources in the class initialization code:
     
     static List foo_list = NULL;
     
     static
     void
     Foo_initialize( Class this )
     {
         ooc_init_class( List ); /* make sure, that List has been initialized */
     
         foo_list = list_new( ooc_delete );
     }
     

You can call ooc_init_class( ClassName ) as many times, you need, the ClassName_initialize( Class ) function will be called only once. (Until ooc_finalize_class( ClassName ) is not called.) You can throw exception in ClassName_initialize( Class ) function.

5.5.3 Class finalization

If you have aquired some global resources during class initialization, you may want to release them before exiting your program. The class finalization method is there for this purpose. The class finalization must not throw an exception!

     
     static
     void
     Foo_finalize( Class this )
     {
         ooc_delete_and_null( & foo_list );
     }
     

It is guaranteed, that ClassName_finalize( Class ) is called only once for each ClassName_initialize( Class ). In most cases the class finalization is just a simple empty function, doing nothing.

5.5.4 Constructor definition

The constructor is responsible for building up an object of the class. The constructor has a fix name: the class name concatenated with _constructor.


In the constructor you can be sure, that all data members are set to 0 (or NULL in case of a pointer) prior calling the constructor.

If your class has a parent class (other than Base) then the first thing in a constructor is calling the parent class's constructor using the chain_constructor() macro! It is advisable putting the chain_constructor() macro always at the begining of your constructor, because this practice makes the task of changing the inheritance more easy. The chain_constructor() macro has three parameters: The class constructor has two parameters: the address of the object itself as an Foo object, and a pointer to the parameters. This parameter pointer was the second parameter of the ooc_new() function, or was assigned by the subclass constructor by the chain_constructor() macro.
     
     static
     void
     Foo_constructor( Foo self, const void * params )
     {
         assert( ooc_isInitialized( Foo ) );   /* It is advisable to check if the class has
                                                  been initialized before the first use */
         chain_constructor( Foo, self, NULL ); /* Call parent's constructor first! */
     
         self->data = * ( (int*) params );
     }
     

If you encounter any problem in the construction code, you can throw an exception here.


It is advisable defining a convenient wrapper around the ooc_new() call to make the parameter type checking perfect and being able to aggregate multiple parameters into a single parameter struct, that can be forwarded to the ooc_new() as the second parameter, and not less importantly converting the returned Object type automatically to your specific object type.
     
     Foo
     Foo_new( int initial_value )
     {
         return (Foo) ooc_new( Foo, & initial_value );
     }
     

5.5.5 Copy constructor definition

The copy constructor creates a second object of your class. The ooc_duplicate uses this constructor when creating a duplicate of the class.


The copy constructor has a fix name: the class name concatenated with _copy.
The copy constructor has two parameters: a pointer to the new object, and a pointer to the object that is copied.
The copy constructor must return: When entering into the copy constructor you can be sure that all the parent class' members are already copied succesfully, and all class members are set to 0 or NULL.

If you encounter any problem in the construction code, you can throw an exception here.
5.5.5.1 Using the default copy constructor

If your class do not require any special action when it is copied (the bit-by-bit copy is OK) then you can leave all the task to the class manager, by simply returning OOC_COPY_DEFAULT:

     
     static
     int
     Foo_copy( Foo self, const Foo from )
     {
         /* makes the default object copying (bit-by-bit) */
         return OOC_COPY_DEFAULT;
     }
     

But be careful with the default copying! Copying pointers may lead unexpected double frees of memory block and may crash! Make your own copy, if you have pointers, reference counted pointers, etc.!


An other aspect is the performance. Because the default copy uses the memcpy() for completing the copy of an object, it is a bit "expensive", it has too much overhead. If your program is using ooc_duplicate() extensively, it is recommended creating your own copy constructor for smaller objects.
5.5.5.2 Creating your own copy constructor

Creating your own copy constructor is simply, and mostly self-explanatory.

     
     static
     int
     Foo_copy( Foo self, const Foo from )
     {
         self->data  =  from->data;
     
         return OOC_COPY_DONE;
     }
     

Do not forget to return OOC_COPY_DONE, otherwise the default copy will run and will overwrite everything that you made!

5.5.5.3 Disabling the copy constructor

Unfortunately it is not possible disabling the copy constructor in compilation time, like in C++. (In C++ this is the technique making the mandatory copy constructor private: Foo::Foo( Foo& ), so noone will be able to access it.)
However you can prevent copying the object in runtime, simply returning OOC_NO_COPY, that forces throwing an Exception with the err_can_not_be_duplicated error code.

     
     static
     int
     Foo_copy( Foo self, const Foo from )
     {
         return OOC_NO_COPY;
     }
     

5.5.6 Destructor definition

The destructor destroys the object of your class before releasing the allocated memory. The ooc_delete uses this destructor when deleting an object.


The destructor has a fix name: the class name concatenated with _destructor.
The destructor has two parameters: a pointer to the object to be detroyed, and a pointer to its virtual table.

Within the destructor you can not throw any exception!
In the destructor you must consider, that your object is not valid: the virtual table pointer was nulled before entering in to the destructor. This is for marking the object that deletion is pending, and preventing multiple entry into the desctructor. (This way we could save some bytes in each objects.) This means that you can not use Virtual macro, or other macros that use the virtual table, e.g. ooc_isInstanceOf().
However you can still access the virtual functions via the vtab parameter, so you can use them if you need. Since ooc 1.0 it is guaranteed that the destructor runs only once. However you should use only ooc_delete_and_null() and ooc_free_and_null() in destructors! This prevents crashes because of double freeing or deleting in case of circular references.
     
     static
     void
     Foo_destructor( Foo self, FooVtable vtab )
     {
         ooc_free_and_null( & self->data_ptr );
     }
     

5.5.7 Implementing class methods

The class methods are normal C functions with the first parameter as a pointer to the object.


Because there is no real parameter type checking in C when calling this class method, it is possible to pass anything to the class method as its first parameter! This is error prone, so it is a good practice to always check the first parameter within the class method!
5.5.7.1 Non-virtual methods

Non-virtual methods are global C functions.

     
     void
     foo_add_data( Foo self, int size )
     {
         assert( ooc_isInstanceOf( self, Foo ) );
     
         self->data_ptr = ooc_malloc( size );
     }
     

5.5.7.2 Virtual methods

Virtual methods are static C functions, that are accessed via pointers in the virtual table.


See section "Virtual Functions" for more information!

5.6 Classes that have other classes

You can have classes that embody other classes. You may implement them as normal objects, and use ooc_new() in the outer objects constructor, to allocate and build the related object, like:

     ClassMembers( Foo, Base )
         Bar    bar;
     EndOfClassMembers;
     
         ....
     
     static
     void
     Foo_constructor( Foo self, const void * params )
     {
         chain_constructor( Foo, self, NULL );
     
         bar = ooc_new( Bar, params );
     }
     
     static
     void
     Foo_destructor( Foo self, FooVtable vtab )
     {
         ooc_delete_and_null( (Object*) & self->bar );
     }

In this example Foo object can be considered, that it has a Bar object as a member. But this way of constructing the Foo object is not effective, because there are two memory allocations: one for Foo and the other for Bar in Foo's constructor. This requires more time, and leads to more fragmented memory. It would be a better idea to include the body of the Bar object completly into the Foo object. You can do it, but must take care, that you must use ooc_use and ooc_release instead of ooc_new and ooc_delete respectively, because there is no need for additional memory allocation and deallocation for the Bar object!
The above example rewritten:

     ClassMembers( Foo, Base )
         struct BarObject    bar;
     EndOfClassMembers;
     
         ....
     
     static
     void
     Foo_constructor( Foo self, const void * params )
     {
         chain_constructor( Foo, self, NULL );
     
         ooc_use( & self->bar, Bar, params );
     }
     
     static
     void
     Foo_destructor( Foo self, FooVtable vtab )
     {
         ooc_release( (Object) & self->bar );
     }

Less malloc(), better performance!
Of course, in this case you can access the member of the included Bar objects a bit different: instead of self->bar->data you must reference as self->bar.data.
Never use the object inclusion for reference counted objects! The reference counting will not work for included objects!