Why C++ is not very fit for GUI programming

 

A paper and a talk presented at MacHack'95,
The 10th Annual Conference for Leading Edge Developers
Southfield, MI, June 22-24, 1995.

 

Abstract

With no intent of starting a holy war, this paper lists several annoying C++ birthmarks that the author has come across developing GUI class libraries. C++'s view of classes, instances and hierarchies appears tantalizingly close to GUI concepts of controls, widgets, window classes and subwindows. OO models of C++ and of a window system are however different. C++ was designed to be a "static" language with a lexical name scoping, static type checking and hierarchies defined at compile time. Screen objects on the other hand are inherently dynamic; they usually live well beyond the procedure/block that created them; the hierarchy of widgets is defined to a large extent by layout, visibility and event flow. Many GUI fundamentals such as dynamic and geometric hierarchies of windows and controls, broadcasting and percolation of events are not supported directly by C++ syntax or execution semantics (or supported as "exceptions" -- pun intended). Therefore these features have to be emulated in C++ GUI code. This leads to duplication of a graphical toolkit or a window manager functionality, code bloat, engaging in unsafe practices and forgoing of many strong C++ features (like scoping rules and compile-time type checking). This paper enumerates a few major C++/GUI sores and illustrates them on simple examples.
 


Listed below is a commented list of snags -- from the weakest upward -- seem inherent in GUI C++ programming. I encountered these problems while wrapping XVT's platform-independent graphical toolkit in a C++ class library. It appears that the pitfalls and workaround kludges are fairly common, to many event- and callback-based systems.

 

 
 

Don't CREATE constructing (or at least, don't show it)

When a derived C++ class overrides a virtual method of its parent the derived class has to keep in mind that the overriding does not take effect while base class' constructor is executing. This feature is especially annoying when objects in question are widgets, which act on GUI events. Suppose a class Basic_Window was designed to create and represent a vanilla black-and-white window on screen:

class Basic_Window {
public:
   Basic_Window(Rect rect) { gt_create_window(rect,visible,this); }
   virtual void handle_create_event() { set_background(WHITE); }
};
Here gt_create_window() stands for a low-level call to the underlying graphical toolkit (e.g., xvt_win_create()). This function allocates toolkit's data structures, notifies a window manager, registers this object to receive events, and in the case above, initiates painting of the window on screen.

Suppose we want to instantiate a Basic_Window -- but with a red background. The canonical way to amend behavior of a class is to derive from it and override appropriate virtual methods. We are tempted to write:

class RedWindow : public Basic_Window {
   virtual void handle_create_event() { set_background(RED); } 
public:
   RedWindow(Rect rect) : Basic_Window(Rect rect)		{}
};
RedWindow red_window(default_rect);

Unfortunately, red_window will show up white, not red! To create a RedWindow, a parent object has to constructed first. After Basic_Window::Basic_Window() completes, RedWindow's virtual tables take effect, the handle_create_event() method becomes overridden, and the RedWindow() constructor is executed. The Basic_Window() constructor registers the object with a graphical toolkit, which instantly starts sending events to the object (e.g., a CREATE event). The Basic_Window() constructor is not guaranteed to finish at this point; virtual method overriding therefore has not yet taken place. Hence the CREATE event will be handled by Basic_Window::handle_create_event(). Virtual tables of the RedWindow class kick in only when the base class is completely constructed, that is, when the window is already on the screen. Changing window color at this point will cause an annoying flash.

There are two easy work-arounds: one is to forbid every constructor to register the object with a graphical toolkit. The event processing will therefore be held off until derived classes finish initializing. If a constructor insists on registration of event listeners, the constructor should at least prevent painting of the window. In that case derived classes will not be denied an opportunity to alter window's appearance before it is shown to the user. These workarounds are effective yet they do not feel right. If an author of a GUI class library violates the above commandments he essentially prevents users of his library from modifying classes' behavior by subclassing. An extra-ordinary insight and forethought are required to design a (re)usable GUI class library. A programmer also has to keep in mind that instantiating of a widget object is not enough to render it on screen. A special method needs to be called to actually show the widget.

It is tempting to regard a widget on screen as a "face" of a GUI application object in memory. As the above discussion indicates this relationship between screen and C++ objects is not straightforward: for one thing, they are born separate.

   

No syntax sugar for event relay

Suppose a GUI class library includes a PictWindow class that displays a picture in a window:

class PictWindow {
   Picture picture;
public:
   virtual void repaint() { gt_draw_pict(picture);  }
   ...
};

We would like to overlay a small rectangle to outline to a particular area of the picture. To this end, we may try to subclass the PictWindow:
class OvWindow : public PictWindow {
   Rect rect;
   virtual void repaint() { gt_draw_rect(rect); }
};

Unfortunately, when OvWindow is instantiated, we will see just a rectangle in a blank window, and no picture. Since OvWindow::repaint() overrides PictWindow::repaint(), the latter function will never be called when the window is to be drawn. We should have implemented OvWindow as:
class OvWindow : public PictWindow {
   Rect rect;
   virtual void repaint() { PictWindow::repaint(); gt_draw_rect(rect); }
public:
   OvWindow(void) : PictWindow() {}
};
The constructor of OvWindow is spelled out to emphasize that the OvWindow::repaint() method should defer to a superclass, just as constructors do. Indeed, a constructor of a derived object first calls the corresponding constructor of a parent object. So should repaint() defer to its uncle: the method in the base class it overrides. In fact, every virtual function that handles GUI events should "honor its uncle". Object construction can be considered sending of a "create" event to a class. C++ compiler "delivers" this event to the base class first. Following that, the compiler propagates the create event down the hierarchy, executing the corresponding constructors in turn. Handling of GUI events has a similar semantics, yet it has to be spelled out explicitly; C++ provides no syntactic sugar in this case.

   

Clashes of hierarchies

C++ offers great help and many a shortcut in representing and implementing of is-a hierarchies. Typical visual objects however -- e.g., a window with buttons, sliders, labels, text areas and other controls and subwindows -- exhibit a container-type, has-a hierarchy. Even when one widget looks like a specialization of another, modelling this with C++ class-derivation hierarchy may be too verbose and inefficient. This is because GUI event flow has more to do with the layout, visibility, attachments and obscuration of visual objects rather than with their pecking order in a is-a hierarchy.

Consider the previous example: a picture window and a small overlaying rectangle. We can implement this example with two separate widgets: a picture widget and a rectangle widget. As the names imply the former paints a picture while the latter draws a rectangle. We configure the rectangle widget to have a transparent background, and super-impose it on the picture widget. The rectangle widget acts as a mixin. Both widgets have separate handlers for DRAW, MOUSE_DOWN etc. events. When the whole window has to be redrawn, the window manager sends a DRAW event to the picture widget and then to the rectangle widget. When only a part of the window has to be repainted, the window manager sends a DRAW event only to affected widgets. Depending on which part of our composite visual object becomes exposed, either both widgets or only one of them will have to handle the DRAW event.

One can be tempted to implement the above example more in C++ spirit. Indeed, a "picture with an outline" widget may be considered a specialization of the picture widget. This ostensibly suggests we should derive from the PictWindow widget class and override a repaint() method:

class OvWindow : public PictWindow {
   Rect rect;
   virtual void repaint()
      { PictWindow::repaint();
        if( rect_needs_repainting() ) gt_draw_rect(rect); }
   ...
};
The OvWindow::repaint() method should call the corresponding method in the base class, PictWindow, and then draw the rectangle. The OvWindow::repaint() will be called to handle every DRAW event directed at our "picture with an outline" window, no matter which part of this window actually needs repainting. The OvWindow::repaint() may draw the rectangle in any case; if it was not necessary, the window manager will clip the rectangle away. In a more advanced implementation, the repaint() method may wish to check first which part of the drawing was affected by the DRAW event and therefore has has to be repainted. The two-widget solution above took advantage of visibility and clipping calculations the window manager performs; the one-widget, OvWindow solution has to (re)implement this functionality on its own. Applying the canonical C++ pattern of class specialization leads in this case to code duplication.

Implementing has-a hierarchies of GUI containers in C++ is also trickier than it appears. Consider for example a typical GUI window with a text field, an OK button, and a scrollbar. Given classes for the controls, the most straightforward representation of this GUI window in C++ is:

class Window {    // Top-level widget
public:
   ...
   int get_height(void) const;		// get the height of the window
   int set_height(const int new_height);
};

class TextField {
   Window& parent; VertScrollBar& sbar;
public:
   TextField(Window& _parent, VertScrollBar& _sbar)
      : parent(_parent), sbar(_sbar)
           // Set this control's size based on parent's height
      { set_size(parent.get_height()); ... }
};

class MyWindow : public Window {
   Button OK;
   TextField text_field;
   VertScrollBar up_down;
   ... 
public:
   MyWindow() : Window(), OK(*this,"OK"),
       text_field(*this, up_down), up_down(*this,text_field)
		{ ... set_height(250); ... }
};

This solution however is wrought with problems. For example, the text_field needs a reference to a scroll bar object, which in turn needs a reference to a text widget it controls. However, when one of them is being constructed the other is not yet initialized! There is also a more subtle problem. The TextField control queries the window height of its container to size itself appropriately. The MyWindow objects sets the dimensions in the constructor. However, the TextField constructor is executed before that of MyWindow. A typical solution is to perform an actual initialization of a widget in a special init() method. One has to remember to call this method after all the information becomes known:

class TextField {
   Window * parent; VertScrollBar * sbar;
public:
   TextField(void) : parent(0), sbar(0) {}
                       // init() is the real "constructor"
   void init(Window * _parent, VertScrollBar * _sbar)
   {  parent = _parent; sbar = _sbar;
      set_size(parent->get_height()); }
};

class MyWindow : public Window {
   Button * OK;
   TextField * text_field;
   VertScrollBar * up_down;
   ... 
public:
   MyWindow() : Window(), OK(0), text_field(0), up_down(0)
   {
     ...
     OK = new Button(*this,"OK");
     text_field = new TextField();
     up_down = new VertScrollBar();
     set_height(250);
     text_field->init(this,up_down);
     up_down->init(this,text_field);
   }
};
The TextField constructor does not actually do anything besides setting all fields in the object to dummy values. The real work of initializing the object is done by the init() method. Note how verbose this solution becomes. It also separates object's creation from object's initialization, thus defeating the purpose C++ constructors were designed to serve. Similar arguments can be made about the order of destruction of the container and its components.

   

To quit to be or not to quit to be

This is the famous problem of what should be destroyed first, a chicken or an egg. Should a DESTROY event handler delete this; Should the destructor of a C++ widget object wipe out that widget off screen? One probably needs both:

class SimpleWindow {
public:
   void handle_destroy_event(void)	{ delete this; }
   ~SimpleWindow(void) 		{ gt_destroy_window(win_id); }
};
SimpleWindow * a_window_ptr = new SimpleWindow(...);

An application or a user may request the window manager or the graphical toolkit to get rid of a widget. For example, the user may click on a "go away" button on window's title bar. In response, the window manager removes the window from the screen, deallocates all related data structures, and sends a DESTROY event to the corresponding application object. This is the last event a C++ window object will get. The handler of the DESTROY event should free all application data related to the window -- eg., the window object. It stands to reason that the handle_destroy_event() method should destruct the object, by doing 'delete this'.

Consider however the consequences of executing

delete a_window_ptr;
in application code. When a C++ window object is being deleted or goes out of scope the corresponding window on screen should be destroyed as well. A window may not live with no one to receive and handle its events. Therefore, the SimpleWindow destructor should ask the graphical toolkit to drop the window by calling an appropriate function -- e.g., xvt_vobj_destroy() or XDestroyWindow(), etc. As outlined above, in the process of disposing of the screen window the graphical toolkit will send a DESTROY event to the C++ object. The handle_destroy_event() handler will then 'delete this;' -- thus attempting to delete the object the second time. A big bummer will ensue.

To avoid these death spirals, a C++ object must keep a "reference counter" so that the object can tell if a DESTROY event had already been received when the destructor is entered. The metaphor "an application window object in memory <-> a window on screen" is slain once again.

   

Out of scope

Lexical name scoping of C++ is not very compatible with the dynamic nature of GUI objects. When an instance of a C++ widget object falls out of its static scope, the object is destroyed. This is often undesirable: for example, in the context

void on_show_button_hit(void)
{
   Picture picture("file.pic");
   PictureWindow window(picture);
} 
one usually wants for the picture to remain on screen after the function finishes. Allocating objects on heap is the only way to let them live beyond the creating scope:
void on_show_button_hit(void)
{
   Picture * picturep = new Picture("file.gif");
   PictureWindow * windowp = new PictureWindow(*picture);
}

When the function exits, the objects Picture and PictureWindow will still be alive; however, their pointers -- picturep and windowp -- will be disposed of. Once object's reference disappears it is rather difficult to destroy the object when it is no longer needed. Therefore, we must ensure that the PictureWindow will self-destruct whenever the window manager tells it to -- and take the Picture with it. As the previous section explained, a PictureWindow instance must maintain a "reference counter" to avoid destructing itself twice. Since C++ does not provide garbage collection facilities, a programmer has to keep (re)implementing them on his own.

   

The Upshot: Major C++/GUI incompatibilities

All in all, this leads to duplication of graphical toolkit or window manager functionality, code bloat, engaging in unsafe practices and forgoing of many strong C++ features (like scoping rules and compile-time type checking).

Of course all these snags are not fatal. C++ is a universal and powerful language; it is Turing-complete and thus capable of expressing every possible computational algorithm. Therefore, if an application demands dynamic features such as those found in Tcl/Tk, Scheme/Tk, Postscript, etc., C++ can always emulate them. On the other hand, why not to use a language where all these features are present by design?


Last updated November 5, 1999

This site's top page is http://okmij.org/ftp/

oleg-at-okmij.org
Your comments, problem reports, questions are very welcome!