Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

Advanced Object-Oriented Features for C/C++


Dr. Dobb's Journal August 1997: Advanced Object-Oriented Features for C/C++

Dynace shows that C++ isn't the only choice

Blake, who is the developer of Dynace, can be contacted at [email protected].


Dynace (pronounced like "dynasty" without the "t") is a development tool that adds to C or C++ object-oriented capabilities that were previously only available in languages such as Smalltalk and CLOS, but without the overhead normally associated with those environments. Dynace is simpler than C++, yet addresses many of the development issues associated with C++. The system, with documentation and full C source code, is freely available at http:// www.edge.net/algorithms/ and from DDJ (see "Availability," page 3).

Dynace itself is written in portable C. It consists of a preprocessor, header files, and a library. Dynace programs are compiled by a normal C/C++ compiler and can use arbitrary C/C++ libraries. The entire system has been ported to DOS, Windows 3.1/95/NT, UNIX, Linux, Macintosh, and VMS. The Windows version includes a development system. Dynace's object-oriented features are based on ideas from CLOS and Smalltalk. In particular, it makes heavy use of metaobjects and dynamic binding. It also features optional run-time garbage collection, multiple inheritance, multiple threads, enforced encapsulation, a complete and well-documented class library, and many example programs.

I wrote Dynace to address some serious shortcomings of C++. In this article, I'll describe how Dynace works and compare it with C++.

An Example

Example 1 is a simple class definition in Dynace. The defclass keyword introduces a class definition that defines two instance variables and one class variable. A new set of instance variables is created for each new object of this class, while a single set of class variables is shared by all objects of the class. In this example, each Person has a name and age, while a single numberOfPeople variable suffices for any number of Person objects. The function definitions define two instance methods (introduced by imeth) and one class method (introduced by cmeth). Class methods only deal with class variables and instance methods typically deal with instance variables as well as class variables. While Dynace methods are defined like C functions, there are a few notable differences. The default type for return values and arguments is object rather than int. I've found it convenient to return self if there is no more natural return type for a method. Another important difference is that each method has an implicit first argument of object self. The declaration of SetName in Example 1 is interpreted as imeth object SetName(object self, char *nm). In fact, Dynace will accept this second form as well, but I find it more legible to omit the first (redundant) argument. As with C++, note that methods can refer to instance or class variables the same way they would refer to local variables.

Example 2 shows how you might use the Person class. A new object can be simply declared as type object. The New() method is used to create a new object of a class. Notice that you invoke a method as an ordinary C function, providing the object as the first argument.

It may not be obvious, but the SetName function called in Example 2 is not the SetName method defined in Example 1. The SetName function is an automatically generated "generic function" that (at run time) determines the class of the object and runs the appropriate method. This "dynamic dispatching" is quite different from the static approach used in C++.

Object Encapsulation

Encapsulation is the combining of code and data in such a way that the only external access is through a well-defined interface. At development time, strong encapsulation helps organize the design and breaks the code into manageable chunks. At debug time, strong encapsulation significantly limits the amount of code you have to examine to locate a problem. After development, strong encapsulation makes code easier for new programmers to understand and limits the amount of code that must be checked and modified when a given module is changed.

You can achieve strong encapsulation in normal C. Example 3 is one way to do this; Example 4 shows how to use the result. (This example doesn't demonstrate inheritance or dynamic binding, two other important object-oriented features.) Notice that nothing external to Example 3 has access to the data elements defined without going through the defined interface. To see the importance of this, imagine that testing reveals that SetName is sometimes called with a string longer than 29 bytes. If any part of the code could directly access the name field, you'd have to search the entire system and add a test at each place the field was changed. However, by using encapsulation, you can make a single change to the SetName function to correct this problem.

C++ supports four types of encapsulation (protection) associated with members of a class: private, protected, public, and friend. Of the four types, public, protected, and friend are ways to circumvent C++'s encapsulation features. And private members are not entirely private, since they are declared in a publicly accessible header file. From a technical standpoint, there is never a reason for protected, public, or friend members. Anything you would ever need to do can be done with private members and access functions. The net effect is that C++, for no apparent technical reason, does not encourage strong encapsulation, nor does it even have the facilities to support it.

Dynace, on the other hand, enforces strong encapsulation. In C++ parlance, all Dynace instance and class variables are private. Dynace achieves this by using techniques similar to those used in Examples 3 and 4. Code that uses an object must call appropriate methods to access any data within the object.

Code in Header Files

Many of C++'s compile-time facilities require you to place code and class definitions in header (.h) files. However, this is also the source of many of C++'s shortcomings. Dynace offers similar compile-time facilities without this requirement. All Dynace class definitions are entirely contained within source (.c) files.

C++ requires class definitions to be placed in header files for three main reasons. The first is so the compiler will know the size of the instance at compile time. The second reason is that C++ needs to know the entire structure of classes at compile time in order to subclass them. The third reason is that the compiler needs to know the precise names and locations of all externally accessible instance members.

Dynace is able to perform these facilities with an increased level of abstraction and code protection without any class definitions in header files. In C++, objects are stored as structures, requiring the compiler to know many details of the class structure at compile time. The only way to access this information is by reading a header file. In Dynace, an "object" is an opaque C pointer, so all data members are private. In addition, method calls are looked up at run time. This means that the compiler only needs to know that it's manipulating an object; everything else is handled at run time.

One important consequence is that a Dynace class can be heavily modified without requiring subclasses of that class or source files that use that class to be recompiled. In C++, even a small change in a class definition necessitates the recompilation of every source file that uses or subclasses that class. In large applications, this can translate into many hours of saved compile time.

Weak Typing

Dynace is a weakly typed language. This means that Dynace treats all objects, including classes, in a generic fashion. Creating generic routines, such as container classes, is trivial in Dynace.

While C++ does all type checking at compile time, Dynace validates objects at run time. At compile time, only the standard C type checking is performed, and "object" is treated as another C data type. Dynace verifies objects at run time, which also catches common problems with uninitialized pointers. An attempt to use an uninitialized object or an incorrect type of object results in an error message, not a hung system as is often the case with C++.

Metaobjects

In C++, classes are static elements that are defined only at compile time. You can't assign classes to variables or pass them as arguments. In Dynace, classes are themselves objects. Classes can be passed to functions, assigned to, or localized, just like any C variable. Dynace classes can even be defined and created at run time.

Remember that every object in Dynace has another object -- called its class -- that defines its behavior. Since classes are objects, they also have classes, called "metaclasses." Internally, they're all just objects. Metaclasses describe functionality associated with classes just like classes describe functionality associated with instances.

This idea, copied from Smalltalk, is already apparent in Example 1. Dynace transparently splits Example 1 into two different classes. The first is the Person class, which defines the two imeth methods and the name and age variables. The metaPerson class defines the numberOfPeople variable and the GetNumberOfPeople method. From a simplified perspective, Person is a subclass of a predefined class named Object, while metaPerson is a subclass of a predefined class named metaObject. Figure 1 illustrates the two-dimensional relationships among these objects.

Based on Figure 1, you can now see exactly how the statement New(Person) works. Person is an object whose class is metaPerson. Since metaPerson doesn't define New(), Dynace looks at its superclass metaObject, which does. Thus, New(Person) is no different than any other method call. Figure 1 also shows how class variables and class methods work. The class variable numberOfPeople is defined in metaPerson, and contained in Person, just as the instance variables name and age are defined in Person and contained in instances of Person (such as the obj variable in Example 2).

This mechanism is both simpler and more flexible than C++, where Person would be a compile-time symbol, and there are special ad hoc mechanisms for customizing the creation of objects (constructors and the operator new).

Generic Functions and Dynamic Dispatch

When you write a method that defines functionality associated with a class, Dynace changes it into a static C function. This accomplishes two things: It prevents any other module from accessing the method, and it allows different modules to use the same method name without causing any link-time problems.

In addition, for each method, Dynace generates a globally accessible function with the same name and arguments, called a "generic function." This generic function is shared by all classes that define a method with the same name, and is used to perform the dynamic dispatching. A user of a class accesses an object's functionality through the generic function.

For example, when the class definition in Example 1 is compiled, the SetName function in that listing becomes a static function, and Dynace automatically creates a global function similar to Example 5. This generic function first calls find_method to locate a pointer to the correct static SetName function, based on the class of this object and a symbolic constant. Dynace also has options to build generic functions using assembly language or C++ inlining for increased performance.

The find_method function is obviously critical to Dynace's performance. The first time a particular method is requested, find_method examines a table of methods for each class and superclass until it finds a match. It then enters the match into a hash table. The next time find_method is called, the match is pulled directly from the hash table.

Dynace's generic functions are slower than the simple indirection used by C++ compilers. However, they're fast enough for most purposes. If speed is a concern, such as when a method is being repeatedly invoked in a tight loop, you can call find_method manually, store the resulting function pointer in a local variable, and call the method directly, with performance comparable to that of C++.

Garbage Collection

Dynace provides gDispose() and gDeepDispose() to release objects when they are no longer needed. Typically, however, you will want to enable Dynace's optional garbage collector. When enabled, the garbage collector periodically scans all objects, marking those that are still in use and freeing those that are no longer needed. Although this may sound rather time consuming, Dynace is able to free many megabytes of objects in the blink of an eye on a normal PC, so performance is not a big issue.

Dynace uses several techniques to make the garbage collection useful and unobtrusive. For example, Dynace reuses the normal object pointers to keep track of the garbage collection process. This trick allows Dynace to do garbage collection without requiring any additional storage -- an essential feature for any routine that's intended to be used only when memory is scarce.

Determining the best time to run garbage collection is an interesting problem. Two obvious approaches are to wait until you run out of memory or to specify a fixed ceiling. The first approach allows the program to consume much more memory than it needs, without regard for other programs or the requirements of the system. The second makes the resulting application inflexible.

Dynace allows you to specify an increment; garbage collection will occur when the heap has grown by this amount. For example, if the increment is 50 KB, the first garbage collection will occur after 50 KB of objects have been created. If there are only 20 KB of live objects after this first cleanup, the next garbage collection will occur when there are 70 KB of objects. This method allows Dynace to meet an application's needs in a practical and flexible manner, while conserving system resources.

Conclusion

Dynace provides C programmers with a natural way to use features usually associated with CLOS, Smalltalk, and Objective-C. Dynace's unique combination of facilities make it an easy and fast alternative to C++ development.

DDJ


Copyright © 1997, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.