Channels ▼
RSS

C/C++

C Programming

Source Code Accompanies This Article. Download It Now.


Nov98: C Programming

Al is a DDJ contributing editor. He can be contacted at astevens@ddj.com.


I'm writing this column from an upscale adobe mountainside home in the eastern end of Santa Fe, New Mexico. This trip is a reunion with my two brothers -- we are usually separated by thousands of miles -- and I've been looking forward to it all year. The home belongs to one of brother Walter's friends, and we are house-sitting while she is away. This is a magnificent setting, quiet and serene. The mountainside and our small canyon are resplendent with piqon, juniper, cottonwood, chamisa, aspen, and an occasional Ponderosa pine. Even though it is now midsummer and most of the southwestern United States are reporting record-breaking temperatures, the temperature here stays in the lower 80s during the day and drops into the 60s at night, perfect for sleeping with the windows open and the covers piled on. Coyotes serenade us to sleep, and the thin air at 7000 feet provides the clearest, brightest view of the sky and stars you can imagine. A full moon tonight adds to the splendor; it's almost too bright to look at. Few places on this earth can compare to the foothills of the Sangre de Christo mountains in the summer.

It is no wonder that Santa Fe attracts a diverse community of artists, musicians, actors, poets, and writers. Walter is himself an artist and writer of considerable talent. Most of his friends are artists, too. Just up the mountain to the east, about 300 yards away and 50 feet above us, is the relatively new mansion of Ottmar Liebert, a guitarist of some prominence. His huge adobe spread offers a vast and magnificent view across the housetops and patios below, including ours, over the town of Santa Fe, to the purple mountains majesty of the west, a perfect setting to inspire Ottmar's soothing guitar improvisations played against new age backgrounds of bird calls, wind chimes, and gently pounding surf.

Brother Julian drove down from Montana in his camper. Judy and I made the cross country trek from Florida in our RV. Walter brought his motorhome up from where he keeps it parked out in the country so we could work on it. His friend's home has a flat spacious back yard that provides ample parking for our travel vehicles and is now cluttered with our tools and the debris from the repairs we have been making. It's also the site of our nightly parties, which can get loud and boisterous when beer flows and brothers have not seen each other for several years with all the outrageous stories to catch up on and reminiscences to share. It looks and sounds like a combination trailer park, heavy equipment repair depot, and fire department picnic. I'll bet Ottmar is thrilled with his new view.

Undo Again

Last year I published in this column a C++ class template library to implement the typical undo operations of interactive programs. I developed the library mainly for small musical notation programs, but I made it generic enough for most interactive applications. The library assumes that the user modifies a document class object and might want to undo those modifications in reverse order.

The applications where I use the library work well with it, but recently I began work on a more comprehensive musical notation program and found the library to be lacking in several ways. First, the library does not support a redo function, and I wanted to include that feature in my new program. Second, the library does not support a generic way to preserve and restore the user's view of the document at the time of the undos and redos. Finally, the library's classes are in the global namespace and might contribute to namespace pollution if used with other libraries.

I spent much of the time during the several day drive to Santa Fe thinking about the undo/redo situation and how I might reimplement it. I wanted to minimize as much as possible any changes in the public interface and was able to achieve that goal successfully, I think. There are some small but significant differences. First, the previous library supports undoable actions with a count of action items to record, whereas the current library supports the recording of only one undoable action. Both libraries support the use of a terminal action node, one that declares itself to represent the first of a sequence of undoable actions to be undone as a set when the user chooses the undo command; removing undo arrays did not represent any loss of functionality. The second difference is found in the use of a placement new operator function to allocate undo action memory. The new library uses the placement new operator notation to associate the undo container class with the undo action node being allocated. This permits the application to disable undo storage, perhaps while inserting objects into the document while reading from a file. Other differences in the library's public interface involve the requirement for the document class to provide functions that support view context switching in addition to the document functions that insert and delete object data in response to undo actions.

It doesn't take much code to implement a generic undo library, but some of the concepts that underpin this small amount of code are complex and enigmatic. Each time I work with this library after a long absence, particularly when I want to modify it, I have to make an effort to relearn how it works. Now that I've written this column, it won't be so difficult the next time. Read these discussions and refer to the code in Listings One and Two (at the end of this article) as you do. Eventually, it all comes clear.

Undo = Stack

Undo is a stack-oriented problem. The user performs an action that inserts, deletes, or replaces one or more objects in a document. The program pushes a node representing each action onto a stack data structure. The node records what is needed to undo the action.

An undo node on the stack represents either an insertion, a deletion, or a replacement of an object in the document. The node saves the data value deleted or replaced and where in the document the data value was prior to the action that could be undone. For insertions the node saves the object location. In the new library, the node saves the data value inserted, too, in order to support the redo feature.

When the user chooses the undo command, the program pops the most recent undo node from the stack and causes the action to be undone by calling cooperating member functions of the document class. To undo an insertion, the program deletes the object that was inserted. To undo a deletion, the program reinserts the deleted object. To undo a replacement, the program replaces the document's object value with the value that existed before the original replacement.

One Do = Several Undos

From the user's perspective, an undo action could involve several actions when viewed from the program's perspective. For example, suppose the user deletes a note from a musical score. The program might delete several objects as a result of that action -- the note itself, an accidental (flat, sharp, natural) associated with the note, ties to other notes, and so on. Then, after deleting all those objects, the program inserts a musical rest to replace the note in order to maintain the metric integrity of the measure. One action for the user translates into several actions for the program. The undo library does not automatically know about these associations. The application program must tell the undo library when an action being pushed is the first of such a sequence; the undo library flags that action's node as the first action in a sequence of pushes and the terminating action in a later sequence of pops. Subsequent actions in the sequence are not flagged when they are pushed. Consequently, when the user chooses the undo command, the program pops and undoes actions until it undoes the one that is flagged as the terminating action.

Undo/Redo = Deque/Stack

Redo is really just the opposite of undo. When the user executes the undo command, the action to be undone is popped from the undo stack and pushed onto the redo stack. I used the Standard C++ std::deque to implement the undo stack and std::stack to implement the redo stack. I'll explain why later.

When users perform an undoable action (a "do"), the program discards all redo actions; they might not be valid any longer. The user's change to the document would likely cause prior redo nodes to have invalid object position information.

An application should not collect an unlimited number of undo nodes in any one interactive session. Users can work all day with one document. If they don't make many mistakes and don't use the undo feature, the undo memory buffer can grow and, eventually, exhaust the memory pool from which it draws. The undo library uses a maximum number of undo actions to manage the undo buffer size. After the maximum number of dos have occurred, the program discards the oldest undo nodes as new nodes are pushed. I used the std::deque container for the undo stack because it allows you to reference and pop from both ends of the container. When the stack gets too deep, the program pops undo nodes from the front of the container and deletes them until the stack is at the required maximum depth. The redo stack does not need to be similarly maintained because it can never contain more nodes than the undo stack contained.

The undo node class uses a placement new operator function to allocate node space from this buffer. I used placement new so that the container could communicate its own object address to the node class's new operator. When a program wants to suppress adding undo nodes to the stack, it tells the node container class to disable undo operations. When the application instantiates objects of undo nodes and undo is disabled, the placement new operator function returns zero rather than a memory address. The undo system knows not to push zero pointers onto the undo stack.

Document View Context

One of the deficiencies of the original library is that it does not provide a generic interface to save and restore any document context other than document content when undo and redo actions are done. For example, the user views the document in a scrolling window and has an insertion cursor pointed somewhere. The user performs a change that is recorded in the undo container. Then the user scrolls elsewhere in the document before performing the undo command. Ideally, the program should restore the user's viewing context as it was prior to the action that is undone. To get a visual sense of the undo command, the program should return the document to its scrolled position when the original action took place. The undo operation might truncate the document such that the insertion cursor is no longer valid and that value should be restored as part of the document's context. Unless the application ensures that these viewing contexts are preserved and properly restored, the user does not see what is expected.

Viewing context is not the only kind of context that one might need to restore but it is sufficient to explain the concept. The new undo library includes calls to document-provided functions to get a parameterized context object when the undo nodes are constructed and to return that object to the document following undo and redo operations. It is up to the document class to provide the object's definition and do something meaningful with it.

The DDJCProgrammingColumnUndo Namespace

I enclosed the undo library in the DDJCProgrammingColumnUndo namespace. Nobody out there can tell me what namespace I am supposed or allowed to use, so I chose DDJCProgrammingColumnUndo quite arbitrarily. It's nice and long, and it has enough prefix data to associate it with this column, so I doubt that there will be any collisions. Unless, of course, I retire some day and a successor columnist coincidentally chooses the same name.

Long namespaces are unwieldy, so my applications use namespace aliases to get them down to something that fits on a line of code.

The Undo/Redo Library

Listing One is undo.h, the source-code file that implements the new undo library. There are two class hierarchies involved. The UndoNode class is templatized by the document class. UndoNode is the base class for all undo actions. UndoNode includes the m_bTerminator data member that indicates whether the node is a terminating node in a sequence of undo actions. The constructor and destructor are protected because the class is used only as a base class. UndoItem is the only class directly derived from UndoNode. UndoItem is parameterized on the document class, the type of the object involved in the undo, and the document context type. It contains a pointer to the object position in the document, a copy of the object inserted, deleted, or replaced, and a copy of the document's context object retrieved at the time the undoable action is added to the undo stack. The UndoItem constructor calls the document class's GetUndoContext function to initialize the parameterized context data member.

UndoInsertNode, UndoDeleteNode, and UndoReplaceNode are derived from UndoItem. These classes are intended to be base classes for ones that the application derives to specialize the undo action node. Each of them contains Undo and Redo functions that the library calls to tell the node to undo or redo itself through cooperation with the document class and to tell the document class to restore its context saved at the time of the action. The library design depends on derived objects of these base classes being instantiated before the action that can be undone occurs. The constructors for UndoDeleteNode and UndoReplaceNode retrieve the data value that is going to be deleted from or replaced in the document. UndoInsertNode waits until the Undo function is called because the data value does not exist in the document before the insert action is taken and when the constructor is executed.

The second class hierarchy starts with the UndoData class, which is parameterized on the document class only. UndoData is meant to be a base class that the application derives from to incorporate the undo data structure into a document. It contains a reference to the document class instance so that the node classes can call the document's undo-related functions. UndoData also contains the undo and redo stacks and the public interface functions that an application calls to add undo nodes and perform undo and redo operations.

Adding Undo to an Application

Listing Two is songundo.h, the source code file from my notation application. This file represents the first step in integrating the undo library into an application. The code in this file is from my application; yours would be different. My application has only one object type that can be inserted, deleted, or replaced in a document. That type is named Event, and there is not much else you need to know about it. Other applications might use char, std::string, other intrinsic or class types, and any mix of them.

The first three classes in songundo.h are derived from UndoInsertNode, UndoDeleteNode, and UndoReplaceNode. (I'm not going to qualify every reference with the undo library's namespace in this discussion. You can see where that qualification is needed by looking at the code.) These derivations instantiate the base class by providing the types that are parameterized. These types are unique to my application. Observe that the calls to the base class constructors pass the object pointed to by a global variable named pFakeBookDoc. This variable is defined by my application and points to the instance of the document class. There are more elegant ways I could have done this, but this way works well enough.

The UndoSongData class is derived from UndoData parameterized by my application's document class. UndoSongData's constructor provides the maximum undo count to the base class constructor. UndoSongData includes member functions that the application calls to store undoable actions. These member functions must each call UndoData::AddUndoNode passing the address of an object of one of the classes derived from UndoInsertNode, UndoDeleteNode, or UndoReplaceNode. This object should be instantiated by using the UndoNode's placement new operator. The this pointer passed to the placement new operator identifies the UndoData derived class so that placement new can access UndoData's enable/disable flag. The undo object address passed to the constructor by the placement new operator is the address in the document where the action is to take place. Observe that no data values are passed. The node constructors get them directly from the document.

With these classes defined, the next step is to incorporate them into the application. My notation program runs under Win32 as a GUI application, but the undo library is designed to be platform-independent. Here are the procedures:

1. Build a document class that represents the document.

2. Include an object of the class derived from UndoData as a data member in the document class. Specify to its constructor the buffer size.

3. Provide the public member functions in Example 1 in the document class. T represents the type of object to be inserted or deleted. The document class needs Insert and Delete functions for each such type. C represents the type of the object that records the document's context.

4. As the user performs actions that change the document, call the appropriate member functions in the class derived from UndoData.

5. When the user executes the application's undo command, call UndoData::UndoLastAction() through the class derived from UndoData.

6. When the user executes the application's redo command, call UndoData::RedoLastUndo() through the class derived from UndoData.

7. To determine if there are any undo or redo actions pending, perhaps to enable and disable the undo and redo commands on a menu or toolbar, call UndoData::IsUndoDataStored() and UndoData::IsRedoDataStored() through the class derived from UndoData. UndoData::IsUndoDataStored() only specifies whether there are undo nodes that can be undone. If the undo count has been exceeded, undo nodes were discarded. The UndoData::WasUndoDataDiscarded function reports that condition. A program can use a combination of UndoData::IsUndoDataStored() and UndoData::WasUndoDataDiscarded to determine if the document needs to be saved.

Without Further Undo...

Judy likes to watch those daytime do-it-yourself and craft shows where former beauty contest winners pretend to be handy around the house. They always have male sidekicks to do the heavy stuff. In virtually every show, the cast demonstrates some arcane procedure such as wiring a breaker box, adding a dormer, sawing a rabbet joint, or installing an attic ventilation fan. I'll close this explanation of an extremely complex subject the way they always end their demonstrations. "And that's all there is to it."

DDJ

<

Listing One

// --------- undo.h#ifndef UNDO_H
#define UNDO_H


</p>
#include <deque>
#include <stack>


</p>
namespace DDJCProgrammingColumnUndo {
//------------------------------------------------------------------------------
// UndoNode<D>: base class for all undo actions


</p>
template <class D> class UndoData;
template <class D>
class UndoNode
{
    bool m_bTerminator;     // undo action stream terminator
protected:
    explicit UndoNode(bool bTerminator) : m_bTerminator(bTerminator) { }
public:
    virtual ~UndoNode() { }
    virtual void Undo(D& rDoc) = 0;
    virtual void Redo(D& rDoc) = 0;
    bool Terminator() const
        { return m_bTerminator; }
    void* operator new(size_t sz, UndoData<D>* pData);
};
//---------------------------------------------------------------------------
// UndoItem: base template class for all undo actions
// D = document class
// T = atomic unit of undo action (string, char, etc.)
// C = document context information
// class D must provide these public functions:
//     void SetUndoContext(C);
//     C GetUndoContext() const;
//     void Delete(T* position);
//     void Insert(T* position, T datum);
//          position = where to delete from/insert into
//          datum = T object to be inserted
// classes T and C must support operator=
//
template <class D, class T, class C>
class UndoItem : public UndoNode<D>
{
protected:
    T*  m_pPosition; // document position of undoable action
    C   m_Context;   // document view context (cursor, e.g.) at time of action
    T   m_Datum;     // data value
    UndoItem(D& rDoc, T* pPosition, bool bTerminator) :
            UndoNode<D>(bTerminator), m_pPosition(pPosition)
        { m_Context = rDoc.GetUndoContext(); }
};
//---------------------------------------------------------------------------
// base class for undoing insertion actions
// instantiate derived class and add to UndoData stack before performing action
template <class D, class T, class C>
class UndoInsertNode : public UndoItem<D,T,C>
{
public:
    UndoInsertNode(D& rDoc, T* pPosition, bool bTerminator) : 
            UndoItem<D,T,C>(rDoc, pPosition, bTerminator)
        { }
    void Undo(D& rDoc)
    {
        // --- save datum for undo/redo
        m_Datum = *m_pPosition;
        // ---- undo the insertion
        rDoc.Delete(m_pPosition);
        rDoc.SetUndoContext(m_Context);
    }
    void Redo(D& rDoc)
    {
        rDoc.Insert(m_pPosition, m_Datum);
        rDoc.SetUndoContext(m_Context);
    }
};
//---------------------------------------------------------------------------
// base class for undoing deletion actions
// instantiate derived class and add to UndoData stack before performing action
template <class D, class T, class C>
class UndoDeleteNode : public UndoItem<D,T,C>
{
public:
    UndoDeleteNode(D& rDoc, T* pPosition, bool bTerminator) : 
        UndoItem<D,T,C>(rDoc, pPosition, bTerminator)
    {
        // --- save datum for undo/redo
        m_Datum = *m_pPosition;
    }
   void Undo(D& rDoc)
    {
        rDoc.Insert(m_pPosition, m_Datum);
        rDoc.SetUndoContext(m_Context);
    }
    void Redo(D& rDoc)
    {
        rDoc.Delete(m_pPosition);
        rDoc.SetUndoContext(m_Context);
    }
};
//----------------------------------------------------------------------------
// base class for undoing replacement actions
// instantiate derived class and add to UndoData stack before performing action
template <class D, class T, class C>
class UndoReplaceNode : public UndoItem<D,T,C>
{
public:
    UndoReplaceNode(D& rDoc, T* pPosition, bool bTerminator) : 
        UndoItem<D,T,C>(rDoc, pPosition, bTerminator)
    {
        // --- save datum for undo/redo
        m_Datum = *m_pPosition;
    }
    void Undo(D& rDoc)
    {
        T temp = *m_pPosition;
        *m_pPosition = m_Datum;
        m_Datum = temp;
        rDoc.SetUndoContext(m_Context);
    }
    void Redo(D& rDoc)
        { Undo(rDoc); }
};
//----------------------------------------------------------------------------
// base class for storing undo actions
// application derives from this class
template <class D>
class UndoData
{
    D& m_rDoc;
    bool m_bDiscardedUndos;
    bool m_bUndoEnabled;
    std::deque<UndoNode<D>*> m_UndoDeque;
    std::stack<UndoNode<D>*> m_RedoStack;
    void DeleteAllRedoActions();
    int m_nMaxUndos;
public:
    UndoData(D& rDoc, int nMaxUndos);
    ~UndoData();
    void AddUndoNode(UndoNode<D>*pUndoNode);// adds action that can be undone
    void UndoLastAction();                  // undoes the most recent action
    void RedoLastUndo();                    // redoes the most recent undo
   void EnableUndo()
        { m_bUndoEnabled = true; }
    void DisableUndo()
        { m_bUndoEnabled = false; }
    bool IsUndoEnabled() const
        { return m_bUndoEnabled; }
    bool IsUndoDataStored() const
        { return !m_UndoDeque.empty(); }
    bool WasUndoDataDiscarded() const
        { return m_bDiscardedUndos; }
    bool IsRedoDataStored() const
        { return !m_RedoStack.empty(); }
    void DeleteAllUndoActions();       // call when saving, loading new, etc.
};
template <class D>
UndoData<D>::UndoData(D& rDoc, int nMaxUndos) : m_rDoc(rDoc), 
    m_bDiscardedUndos(false), m_bUndoEnabled(true), m_nMaxUndos(nMaxUndos)
{
}
template <class D>
UndoData<D>::~UndoData()
{
    DeleteAllUndoActions();
    DeleteAllRedoActions();
}
template <class D>
void UndoData<D>::DeleteAllUndoActions()
{
    while (!m_UndoDeque.empty())    {
        delete m_UndoDeque.back();
        m_UndoDeque.pop_back();
    }
}
template <class D>
void UndoData<D>::DeleteAllRedoActions()
{
    while (!m_RedoStack.empty())    {
        delete m_RedoStack.top();
        m_RedoStack.pop();
    }
}
template <class D>
void UndoData<D>::AddUndoNode(UndoNode<D>* pUndoNode)
{
    if (pUndoNode != 0) {
        // --- clean up the undos saved for possible redos
        DeleteAllRedoActions();
        // --- prevent the undo deque from growing too large
        if (m_UndoDeque.size() >= m_nMaxUndos)  {
          while (!m_UndoDeque.empty() && (m_UndoDeque.size() >= m_nMaxUndos || 
                    m_UndoDeque.front()->Terminator() == false))    {
                delete m_UndoDeque.front();
                m_UndoDeque.pop_front();
            }
           m_bDiscardedUndos = true;
        }
        m_UndoDeque.push_back(pUndoNode);
    }
}
template <class D>
void UndoData<D>::UndoLastAction()
{
    bool bTerminal = false;
    while (!bTerminal && !m_UndoDeque.empty())  {
        m_UndoDeque.back()->Undo(m_rDoc);
        m_RedoStack.push(m_UndoDeque.back());
        bTerminal = m_UndoDeque.back()->Terminator();
        m_UndoDeque.pop_back();
    }
}
template <class D>
void UndoData<D>::RedoLastUndo()
{
    bool bTerminal = false;
    while (!bTerminal && !m_RedoStack.empty())  {
        m_RedoStack.top()->Redo(m_rDoc);
        m_UndoDeque.push_back(m_RedoStack.top());
        m_RedoStack.pop();
        if (!m_RedoStack.empty())
            bTerminal = m_RedoStack.top()->Terminator();
    }
}
// ---- operator placement new supports undo enable/disable
template <class D>
void* UndoNode<D>::operator new(size_t sz, UndoData<D>* pData)
{
    void* p = 0;
    if (pData->IsUndoEnabled())
        p = ::operator new (sz);
    return p;
}
} // namespace DDJCProgrammingColumnUndo
#endif

Back to Article

Listing Two

// ------- songundo.h

</p>
#ifndef SONGUNDO_H
#define SONGUNDO_H


</p>
#include "undo.h"
#include "FakeBookDoc.h"


</p>
namespace undo = DDJCProgrammingColumnUndo;


</p>
// --------------------------------------------------------------------------
class UndoInsertEvent : public 
        undo::UndoInsertNode<CFakeBookDoc, Event, CFakeBookDoc::ViewContext>
{
public:
    UndoInsertEvent(Event* pEvent, bool bTerminator) : 
        undo::UndoInsertNode<CFakeBookDoc, Event, CFakeBookDoc::ViewContext>
            (*pFakeBookDoc, pEvent, bTerminator)
        {   }
};
// --------------------------------------------------------------------------
class UndoDeleteEvent : public 
        undo::UndoDeleteNode<CFakeBookDoc, Event, CFakeBookDoc::ViewContext>
{
public:
    UndoDeleteEvent(Event* pEvent, bool bTerminator) : 
        undo::UndoDeleteNode<CFakeBookDoc, Event, CFakeBookDoc::ViewContext>
            (*pFakeBookDoc, pEvent, bTerminator)
        {   }
};
// --------------------------------------------------------------------------
class UndoReplaceEvent : public 
        undo::UndoReplaceNode<CFakeBookDoc, Event, CFakeBookDoc::ViewContext>
{
public:
    UndoReplaceEvent(Event* pEvent, bool bTerminator) : 
        undo::UndoReplaceNode<CFakeBookDoc, Event, CFakeBookDoc::ViewContext>
            (*pFakeBookDoc, pEvent, bTerminator)
        {   }
};
// --------------------------------------------------------------------------
class UndoSongData : public undo::UndoData<CFakeBookDoc>
{
public:
   explicit UndoSongData(int nMaxUndos) : 
                undo::UndoData<CFakeBookDoc>(*pFakeBookDoc, nMaxUndos)
        {   }
   void AddInsertEventUndo(Event* pEvent, int nCount, bool bTerminator = true)
   {
        for (int i = 0; i < nCount; i++)    {
            AddUndoNode(new (this) UndoInsertEvent(pEvent++, bTerminator));
            bTerminator = false;
        }
   }
   void AddDeleteEventUndo(Event* pEvent, int nCount, bool bTerminator = true)
    {
        for (int i = 0; i < nCount; i++)    {
            AddUndoNode(new (this) UndoDeleteEvent(pEvent, bTerminator));
            bTerminator = false;
        }
    }
    void AddReplaceEventUndo(Event* pEvent, bool bTerminator = true)
    {
        AddUndoNode(new (this) UndoReplaceEvent(pEvent, bTerminator));
    }
};
#endif

Back to Article


Copyright © 1998, 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.
 

Video