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

Database

Relational C++ Objects


Dr. Dobb's Sourcebook September/October 1997: Relational C++ Objects

Eric is a system software engineer for IBM's AS/400 division. He can be contacted at [email protected].


The object model facilitates large system design by forcing atomic data structuring that optimally mimics the actual interactions in the real world. Object-oriented database systems provide object-oriented modeling and object persistence -- permanent object storage. The problem with these database systems is not in their inherent ability, but the lack of standards in the industry -- neither the language nor the object storage formats are standardized.

In this article, I present a database system that builds on C++ by adding a set of object classes that provide an object-oriented modeling environment that stores data inside industry-standard relational database systems. This system overcomes the inherent problem of mapping a hierarchical object schema to flat storage. The current implementation runs on top of Windows and uses ODBC to access any available relational database system.

Persistence is provided by the framework classes that are used to define the derived classes in the application domain. The design model dictates that objects be defined as sets of interfaces, which themselves define sets of characteristics. In this model, inheritance is defined by the aggregation of interface definitions. Polymorphism is dynamic, as the set of interfaces that describe any single object can be modified at run time.

The complete database class framework is available electronically; see "Programmer's Services," page 3. All you need to design and implement a database application is this framework, a Win32 compiler, and an ODBC database engine (such as the Jet database engine included with Microsoft Access).

Database-System Design

My major objective in designing this object database system was to provide a mechanism for using preexisting relational database systems to support object-oriented database-application development. Since Windows was the target platform, access to SQL databases via ODBC seemed most appealing. Applications written using the ODBC API need no modification or recompilation to access any supporting database system (all major commercial database vendors provide ODBC drivers).

Database designs conform to an interface model that defines the object-relational storage interaction. The interface model provides both a sound design methodology and practical object abstraction that maps well to the flat storage provided by relational database systems. The characteristics of an object are defined by associating appropriate interfaces. An interface maps directly to a flat relational table with fields that represent the data members of the interface. Since an object is constructed by a number of interfaces, a single object may span a number of relational tables. In the C++ domain, objects are derived from the DBObject base class, and associated interfaces from the DBInterface base class.

For example, a basic animal class definition contains an interface that describes the generic "animal." A dog object would be derived from animal, but would also contain the dog interface containing that which describes "dogness."

In short, an object is defined for each representative entity in the application domain. An object is defined by a set of attributes that are grouped together in interfaces. In essence, an interface defines "characteristicness."

Queries return objects. The DBObjectQuery class provides the query mechanism used for all data searches. An initial query is made by using one of the first two methods in Example 1. DBObjectQuery forms the correct SQL statement to return a single object identifier for each item in the result set. The methods create a new instance each time they are invoked. The attributes for the returned objects are accessed by requesting the appropriate interface from the objects. Since the system creates a separate relation table for each type of interface, SQL queries can be made on the database using the name of the interface and the names of its members.

The three classes in Listing One define a person, student, and book. A student is also a person and is derived appropriately. The respective interfaces define the characteristics of the objects with similar names. Figure 1 shows how instances of the objects would be stored in a database.

Listing Two prints all students born before 1972. The query requests the objects that have entries in the student interface table (GetFirst(Student)). The objects that are returned, then, are certain to at least support the Student interface. For each student object, its Person interface is requested. The person's name -- defined by the Person interface -- is then printed.

Any valid SQL query can be used to define the object result set. Listing Three prints all authors who have written books for "Random House." Authors are identified by the system-generated object identifier Persons.ID. The Query class detects the primary object in the From clause and returns objects of that type; namely, objects that support the primary interface Person.

Database-System Implementation

A table is created in the relational database system for each type of interface defined. The first field of each interface record is an instance identifier (ID) that identifies the object to which the interface belongs. At run time, when a C++ object is generated, the system loads the list of interfaces that belong to the object. When the program requests an interface for an object, the system loads the data from the database as needed.

A Database class maintains complete database session-state and connection information by encapsulating ODBC calls. A reference to the Database class is propagated throughout the application. Besides maintaining the connection, the Database object is the source of new SQL Statement objects and is the controller of nested transactions. Transaction requests made by a statement are accumulated by the Database object until the number of commits equals the number of transaction starts.

All SQL interaction occurs via Statement objects. Besides acting as a gateway to the relational database system, Statements maintain query-state information for those operations where multiple item-result sets are a consequence.

A single class maintains system state and controls interaction between all the objects and interfaces in a database. The ObjectDatabase derives from the Database class and defines the storage location for the objects. The ObjectDatabase class tracks objects and interface class types, and is used to create object identifiers and generate interface instances.

When an application initializes, it creates an instance of the ObjectDatabase, which it connects to an ODBC data source. The application further registers interface types with the class. The methods of the ObjectDatabase are not usually accessed by application programs, but the instance is required for creating new objects or generating queries.

The DBObject is the basic unit for operations. The DBObject controls all persistence and manages the interfaces that define the object. Each object instance is assigned a unique identifier by the system. This identifier is placed in the ID field of each interface record that is associated with the object.

Before an object goes out of scope, a call must be made to the object's Save() method to cause the object to be written to the relational database system. It is possible to place Save() in the destructor of derived objects to enable an automatic save operation when the object loses scope. Listing Four presents simplified save code. The procedure iterates through a linked list of interface information records that are attached to the object. Each interface is written to the database, in turn, using ODBC calls. The binding record contains information about the interface class structure used to create the SQL update or create statements.

A persistent object is restored automatically either by a query to the database or by explicitly constructing an object with a specific ID. An object's persistence can be removed by calling Delete().

To refresh the memory image of all interfaces registered with an object call, a Reload() method is provided. This function has the artifact of guaranteeing a complete copy of an object in memory, which may not be the case during normal operation, since interface data is usually loaded only as needed.

Interfaces of an object are accessed with the GetInterface(classId) method. The method returns an interface instance if classId is a member of the object, otherwise it returns NULL. If the interface is a member of the object but the underlying database system fails to load it, the method will throw an exception. Listing Five shows simplified GetInterface() method code. The procedure attempts to retrieve the interface from the object's cache, but loads complete interface information if necessary. The presence of the interface record does not necessarily guarantee that the interface object has been loaded from the database. GetInterface() creates the interface class instance if necessary and fills in its members from the database before returning.

Dynamic polymorphism is supported through the use of methods that dynamically add and remove interfaces (characteristics) to and from objects. The AddInterface() method dynamically adds the characteristic defined by the interface to the object. Similarly, RemoveInterface() can be used to delete an interface. For example, consider the object that describes a person who initially enters the system as a student, but then becomes a faculty member. The student could become a faculty member by removing the student interface and adding the faculty interface.

Interface Class Factories

The class factory is the most important component of the interface model. Each object is defined by a set of interfaces that describe the object's characteristics. When an interface for the object is requested, the database system must be able to produce the C++ interface instance. Class factories are generated when the interface classes are defined, and registered with the ObjectDatabase at application startup. When a request for an interface is made, the ObjectDatabase searches its list of class factories for one with an equal class identifier. The matching factory is requested to generate the appropriate interface instance.

The class factory also maintains all interface type information. This includes binding information and a SQL statement cache for the most-often used SQL statements -- those used to save and restore object data to the relational database system.

SQL Interaction and Binding Records

Bindings describe how an interface is stored in a table. A binding table can be created manually, but macros have been designed to simplify the process. For each member of the interface class, a binding is to be defined using Listing Six.

The second macro is used for variable-sized objects, like strings. The macros compute the storage offset location for the indicated member's class using a multiple-inheritance-safe formula; see Listing Seven (where _Locator is the first member of the base DBInterface class).

With the relative storage location information, the SQL driver can place object data directly at the memory location occupied by the interface. The interface example in Listing Eight defines a complete definition.

An object may contain a collection of other objects. DBSetInterface is a persistent container for other objects. An object simply registers DBSetInterface as a member interface during construction. The interface provides four primary methods; see Example 2. The first two are self explanatory; the third and fourth provide a means of iterating the set.

It is possible to guarantee atomic actions on an object by wrapping all critical code between calls to the Take() and Release() methods. Take() attempts to obtain the object's critical section; upon failure, a specified number of additional attempts are made -- each followed by a delay. Using object-critical sections, object operations can safely span multiple threads and processes.

The critical section implementation uses the fact that the relational-database system will not create two records having the same key. Take() attempts to create a record in the mutual exclusion table with a key equal to the object instance identifier. If the operation is successful, the critical section is owned. Another attempt to create a record with the same object will fail. Release() simply removes the record that represents the critical section.

All object operations are handled as transactions to guarantee that an object's persistent image is always in a consistent state. The nesting transaction ability of the Database class permits transactions to span multiple objects and databases.

The Reservation Application

The reservation application consists of two parts. The first part is a complete reservation system, independent of an interface. The second part is a Windows GUI application. The separation facilitates changing the interface or porting the application. Because the database system is simply a set of C++ classes, no additional work is needed to merge the database and application domains.

Conceptually simple, the two primary elements are objects that represent each piece of equipment. Each equipment item contains its own list of reservations. Each reservation is associated with the person who reserved the item and with the location at which it will be used. The actual database contains a number of other classes that maintain information about the original equipment owner, manufacture, and description fields.

Each type of equipment is derived from a generic equipment base class. The database model usually requires the definition of at least two classes per object. One class represents the object itself, the others are the interfaces that the object supports. The generic Equipment class supports the IEquipment and IScheduleSet interfaces. IScheduleSet defines a container for Schedule objects -- the reservations. Each type of equipment is derived from the Equipment class and supports an additional interface defining the extension properties.

Most reservation system operations are encapsulated in the Scheduler class; Listing Nine shows the methods of this class.

Queries find equipment items with schedules having the defined properties. The query is dynamically constructed from only those parameters which are specified (that is, not NULL). Listing Ten is the constructed query statement.

Although similar, searching for available equipment is complicated by the fact that each item must not contain a reservation in the requested time frame. The derived DBObjectQuery class that is used for available equipment queries overrides a filter method that is called to verify the inclusion of each item as it is returned from the SQL query. The filter checks for the nonexistence of a reservation in the requested time frame using Listing Eleven.

For demonstration purposes, I wrote a simple Win32 GUI interface for making reservations and running queries. The main screen in Figure 2 is a menu for the three primary operations -- Reserve, Query, and Maintenance. Both the reservation and query buttons open the window in Figure 3. The maintenance mode allows the administrator to define new equipment.

To make a reservation, the user selects the type of equipment, and the time and location for which he desires the equipment. The program returns with a list of relevant items; see Figure 4. From the list, the user selects the item or items he would like to reserve.

Information about any listed item can be brought forth by pressing the Details button to bring up a window of properties similar to the one in Figure 5. It should be straightforward to extend the output so that it provides a type-sensitive display of properties; that is, computer properties would differ from those of a television. The basic equipment property screen shows a list of current reservations.

DDJ

Listing One

class PersonInterface : public DBInterface    {
    char Name[50];
    DATE Birthday;
    }
class Person : public DBObject
    {
    Person() {AddInterface(PersonInterface);}
    }
class StudentInterface : public DBInterface
    {
    char Major[50];
    }
class Student : public Person
    {
    Student () {AddInterface(StudentInterface);}
    }
class BookInterface : public DBInterface
    {
    char Title;
    INSTID Author;
    char Publisher[50];
    }
class Book : public DBObject
    {
    Book() {AddInterface(BookInterface);}
    }

Back to Article

Listing Two

DBObjectQuery Query(ODB);student = Query.GetFirst(Student, " Birthday < 1972");
while (student)
    {
    Iperson = studentrGetInterface(Person);
    cout << Iperson->GetName();
    student = Query.GetNext();
    }

Back to Article

Listing Three

DBObjectQuery Query(ODB);From = "Persons JOIN Books ON Persons.ID = Books.Author";
Where = "Books.Publisher = 'Random House'";
current = Query.GetFirst(From,Where);
while (current)
    {
    Iperson = current->GetInterface(Person);
    cout << "Author: " << IpersonrGetName();
    current = Query.GetNext();
    }

Back to Article

Listing Four

struct InterfaceRecord : public LinkList<InterfaceRecord>    {
    flag Loaded;
    CLASSID ClassID;
    flag NewInterface;
    DBInterface *Interface;
    };
bool DBObject::Save()
    {
    bool Status = true;
    InterfaceRecord *Record;
    ODB.BeginTransaction();
    try {
        Record = Interfaces.GetFirst();
        while (Record && Status)        // Save each interface.
            {
            Status = SaveInterface(Record);
            Record = Interfaces.GetNext(Record);
            }
        }
    catch (...)
        {
        Status = false;
        }
    if (Status) ODB.Commit();
    else ODB.Rollback();
    return Status;
    }
bool DBObject::SaveInterface(InterfaceRecord *Record)
    {
    bool Status = true;
    DBInterface *Interface = Record->Interface;
    if (Interface && Record->Loaded)
        {
        Status = Interface->Update();   // Save preexisting interface.
        }
    else    {
        if (!Interface)                 // Need to access interface.
            {
            CLASSID ClassID = Record->ClassID;
            Interface = ODB.CreateInterface(ClassID,InstID);
            Record->Interface = Interface;
            }
        if (Record->NewInterface)   // Create new interfaces.
            {
            Status = Interface->Create();
            Record->NewInterface = false;
            Record->Loaded = true;
            }
        }
    return Status;
    }

Back to Article

Listing Five

DBInterface* DBObject::GetInterface(CLASSID ClassID)     {
    InterfaceRecord *Record;
    Record = SearchInterfaceCache(ClassID);
    if (!Record)                        // Not in cache.
        {
        LoadInterfaceList();
        Record = SearchInterfaceCache(ClassID);
        if (!Record) return NULL;           // Not a member.
        }
    if (!Record->Interface)             // No instance exists...
        {
        Record->Interface = ODB.CreateInterface(ClassID,InstID);
        Record->Interface->Init();
        }
    if (!Record->Loaded && !Record->NewInterface)
        {
        Record->Interface->Load()       // Load data from DB.
        Record->Loaded = true;
        }
    return Record->Interface;
    }

Back to Article

Listing Six

BINDING(Class, Member, Type, Index)    Class = Name of the interface.
    Member = Name of the class member.
    Type = String, Date, Double, Long
    Index = Controls whether an index is created on the field.
VAR_Binding(Class, Member, Type, Size, Sizemember, Index)
    Size = Bytes of member.
    Sizemember = Variable to recieve member's active size.

Back to Article

Listing Seven

ComputeOffset(Class,Member)     ((long)(&((Class *)0)->Member) +
         (long)(&((DBInterface *)0)->_Locator) - 
               (long)(&((Class *)0)->_Locator))

Back to Article

Listing Eight

class IAnimal : public DBInterface    {
    private:
        DWORD _Sz;
        char Name[30];
        unsigned long Age;
    public:
        INIT_INTERFACE(IAnimal);
        void SetData(Name,Age);
        char* GetName();
        unsigned long GetAge();
    };
BEGIN_BINDING_TABLE(IAnimal,2);
    VAR_BINDING(IAnimal,Name,FT_String,30,_Sz,true);
    BINDING(IAnimal,Age,FT_Long,false);
END_BINDING_TABLE;

Back to Article

Listing Nine

Scheduler(ObjectDatabase &ODB);flag AddReservation(Equipment &Item, Schedule &Reservation);
flag RemoveReservation(Equipment &Item, DATETIME &StartTime);
flag RemoveReservation(Equipment &Item, Schedule &Reservation);
Equipment* QueryAll(CLASSID EQtype, DATETIME *Start, DATETIME *End,
            User *ByWhom=NULL, Location *Place=NULL);
Equipment* QueryAvailable(CLASSID EQtype, DATETIME &Start, DATETIME &End);
Equipment* QueryNext();

Back to Article

Listing Ten

SELECT Equipment.ID FROM (IEquipment INNER JOIN IScheduleSet ON IEquipment.ID = 
IScheduleSet.Owner) INNER JOIN ISchedule ON 
IScheduleSet.Member = ISchedule.ID WHERE 
(ISchedule.StartTime >= 12/9/1996) AND 
(ISchedule.EndTime <= 12/9/1996 0:0) AND 
(ISchedule.Who = 752) AND 
(ISchedule.Place = 4) AND 
(IEquipment.Type = 2004)

Back to Article

Listing Eleven

flag Equipment::IsAvailable(DATETIME &Start, DATETIME &End)    {
    char DateQuery[100];
    IScheduleSet *Schedules;
    ReservedString(DateQuery,Start,End);
    Schedules = (IScheduleSet *)GetInterface(ScheduleSetID);
    if (!Schedules) return false;
    DBObject *Item = Schedules->GetFirst(ScheduleID,DateQuery);
    if (Item) delete Item;
    return Item == NULL;        // True if no Reservation.
    }

Back to Article

Listing Twelve

// Object Database Test

</p>
#include <stdio.h>
#include <conio.h>
#include <sqlobj.h>


</p>
#define PersonID 1001
#define ChildSetID 1002


</p>
class Person : public DBObject
    {
    public:
        Person(ObjectDatabase &ODB) : DBObject(ODB)
            {
            AddInterface(PersonID);
            AddInterface(ChildSetID);
            }
    };
class Children : public DBSetInterface
    {
    public:
        INIT_SETINTERFACE(Children);
        flag AddPerson(Person *p) {return AddObject(p);}
        flag RemovePerson(Person *p) {return RemoveObject(p);}
    };
class PersonInterface : public DBInterface
    {
    private:
        char Name[30];
        DWORD _NameSize;
        unsigned long Age;
    public:
        INIT_INTERFACE(PersonInterface);
        void SetData(char *name, long age)
            {
            strcpy(Name,name);
            Age = age;
            }
        char* GetName() {return Name;}
        unsigned long GetAge() {return Age;}
    };
BEGIN_BINDING_TABLE(PersonInterface,2);
    VAR_BINDING(PersonInterface,Name,FT_String,30,_NameSize,true);
    BINDING(PersonInterface,Age,FT_Long,false);
END_BINDING_TABLE;


</p>
char *Names[] = {"Beth","Debbie","Bob","Mary","John","Laura",NULL};
long Ages[] = {43,13,12,21,6,10,0};


</p>
void ComplexQuery(ObjectDatabase &ODB, char *ChildName)
    {
    DBObject *Current;      // Prints the parents of a Child.
    char *From, Where[100];
    PersonInterface *IPerson;
    DBObjectQuery Query(ODB);
    From = "(PersonInterface AS Parent "
           "INNER JOIN Children ON Parent.ID = Children.Owner) INNER JOIN "
           "PersonInterface AS Child ON Children.Member = Child.ID ";
    sprintf(Where,"Child.Name = '%s'",ChildName);
    Current = Query.GetFirst(From,Where);
    while (Current)
        {
        IPerson = (PersonInterface *)Current->GetInterface(PersonID);
        printf("Parent: %s\n",IPerson->GetName());
        delete Current;
        Current = Query.GetNext();
        }
    return;
    }
void View(ObjectDatabase &ODB)          // Prints the names of Beth's
    {                                   // children who are older than 12.
    DBObject *Parent;
    Children *IChildren;
    PersonInterface *IPerson;
    DBObjectQuery Query(ODB);
    Parent = Query.GetFirst(PersonID,"Name='Beth'");
    IPerson = (PersonInterface *)Parent->GetInterface(PersonID);
    IChildren = (Children *)Parent->GetInterface(ChildSetID);
    printf("Name: %s\n",IPerson->GetName());
    printf("Age: %lu\n",IPerson->GetAge());
    DBObject *Child = IChildren->GetFirst(PersonID,"Age >= 12");
    while (Child)
          {
          IPerson = (PersonInterface *)Child->GetInterface(PersonID);
          printf("Child: %s (%lu)\n",IPerson->GetName(),IPerson->GetAge());
          delete Child;
          Child = IChildren->GetNext();
          }
    delete Parent;
    return;
    }
void Make(ObjectDatabase &ODB)          // Adds users to the database
    {                                   // making the first the parent
    Person Parent(ODB);                 // of all the others.
    Children *IChildren;
    PersonInterface *IPerson;
    IPerson = (PersonInterface *)Parent.GetInterface(PersonID);
    IChildren = (Children *)Parent.GetInterface(ChildSetID);
    IPerson->SetData(Names[0],Ages[0]);
    Parent.Save();
    for (int i=1; Names[i]; i++)
        {
        Person Child(ODB);
        IPerson = (PersonInterface *)Child.GetInterface(PersonID);
        IPerson->SetData(Names[i],Ages[i]);
        IChildren->AddPerson(&Child);
        Child.Save();
        }
    Parent.Save();
    return;
    }
main()
    {
    ObjectDatabase ODB("Persons","","");
    ODB.Connect();
    REGISTER_INTERFACE(ODB,PersonInterface,PersonID);
    REGISTER_SETINTERFACE(ODB,Children,ChildSetID);
    Make(ODB);
    View(ODB);
    ComplexQuery(ODB,"John");
    ODB.Disconnect();
    getch();
    return 0;
    }

Back to Article

Listing Thirteen

// Reservation System #include <assert.h>
#include <stdio.h>
#include <conio.h>
#include <sqlobj.h>


</p>
#pragma hdrstop
#include <schedule.h>
void PrintSchedule(Equipment &Item)
    {
    IScheduleSet *Reservations;
    Reservations = (IScheduleSet *)Item.GetInterface(ScheduleSetID);
    if (!Reservations) return;
    Schedule *Reservation = (Schedule *)Reservations->GetFirst();
    while (Reservation)
          {
          ISchedule *iReservation;
          iReservation = (ISchedule*)Reservation->GetInterface(ScheduleID);
          if (iReservation)
            {
            char Start[100], End[100];
            DateToString(Start,iReservation->GetStartDate());
            DateToString(End,iReservation->GetEndDate());
            printf("     Reserved: %s TO %s\n",Start,End);
            }
          delete Reservation;
          Reservation = (Schedule *)Reservations->GetNext();
          }
    return;
    }
void PrintEquipment(ObjectDatabase &ODB)
    {
    Scheduler Reservations(ODB);
    Equipment *equipment = Reservations.QueryAll(EquipmentID);
    while (equipment)
        {
        IEquipment *iEquipment;
        iEquipment = (IEquipment*)equipment->GetInterface(EquipmentID);
        if (iEquipment)
            {
            printf("Item: %s\n",iEquipment->GetDescription());
            PrintSchedule(*equipment);
            }
        delete equipment;
        equipment = Reservations.QueryNext();
        }
    return;
    }
void PrintUsers(ObjectDatabase &ODB)
    {
    DBObjectQuery Query(ODB);
    User *user = (User *)Query.GetFirst(UserID);
    while (user)
        {
        Users *iUser;
        iUser = (Users *)user->GetInterface(UserID);
        if (iUser) printf("User: %s\n",iUser->GetName());
        delete user;
        user = (User *)Query.GetNext();
        }
    return;
    }
void Test(ObjectDatabase &ODB)
    {
    Scheduler Reservations(ODB);
    Equipment Sony(ODB,"Sony Monitor");
    Sony.Save();
    Computer IBM(ODB,"IBM Computer");
    Schedule Reservation(ODB,NULL,NULL,"1/2/1996","2/3/1997");
    Reservations.AddReservation(IBM,Reservation);
    Reservations.RemoveReservation(IBM,"1/2/1996");
    Reservation.Save(); 
    IBM.Save();
    PrintEquipment(ODB);    
    return;
    }
main()
    {
    ObjectDatabase ReservationODB("Reservations","","");
    flag Status = ReservationODB.Connect();
    if (!Status)
        {
        printf("\nCan't Connect To Database\n");
        getch();
        }
    RegisterInterfaces(ReservationODB);
    Test(ReservationODB);
    getch();
    ReservationODB.Disconnect();
    return 0;
    }

Back to Article


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.