Ch18.8: Qt, MFC, and Why GUIs Are Where C++ Went Wrong
1. The Most Overused Feature in the Most Overused Domain
virtual and new are the most overused features in C++. The place where they are most
overused is the graphical user interface, and the two libraries that did the most to
normalise it are Qt and MFC. If you learn C++ to write desktop applications, this is
almost certainly the style you will be taught first, and it is the style this chapter
exists to argue against.
The reason is historical, and it was reasonable in 1992: a window is a thing whose
behaviour varies by kind, events arrive from outside, and the natural modelling tool is a
base class with virtual event handlers, plus new because widgets outlive the function
that created them. Three decades later the cost of that decision is visible, and the
alternative (a web front end talking to a native service) did not exist yet.
2. What Qt Does, Concretely
Strip away the polish and Qt is this:
- Every widget inherits from
QObject, a polymorphic base with a virtual destructor. Every widget is therefore heap-allocated, every widget carries a vptr, and every widget lives in a parent/child ownership tree that deletes its children for you — a hand-builtunique_ptrforest, with the same problems and a longer lifetime. Q_OBJECTandmoc. Qt's meta-object system is generated by an external code generator that parses your headers and emits astaticMetaObject: a string table of class names, a method table, a signal/slot table, and a pointer to the base class's meta-object. That is a hand-rolled, string-based RTTI, kept in every one of your classes, and it is a dependency on a build step that must run before the compiler.qobject_castis Qt'sdown_cast. To its credit, it is cheaper thandynamic_cast: it walks a chain ofstaticMetaObjectpointers instead of the C++ typeinfo graph, and it is the right idea (Ch18.5). It is also string-keyed, works only onQObjects, and needs the moc tables to exist — a second dispatch and type system in your program, next to the C++ one.- Signals and slots are a dynamic binding layer on top of all that, with a runtime connection table per object.
- One ABI for your whole program. Qt is not header-only and cannot be consumed as a value library: your binary is linked against one Qt version, built with one compiler, with one set of flags, and mixing two Qt builds in one process is unsupported by construction.
None of that is incompetence. It is what a 1995 design costs when it is maintained into 2026 and the language still cannot express "a list of callbacks and a type tag".
3. What MFC Does, Concretely
CObjectandCRuntimeClass: MFC's hand-rolled RTTI, declared byDECLARE_DYNAMIC, defined byIMPLEMENT_DYNAMIC, queried withIsKindOf(). It is a linked list of class descriptors, compared by name at run time.- Message maps instead of virtual functions for window messages: macros
that build a static table mapping message IDs to member functions. A second dispatch
mechanism next to the vtable, because virtual functions were too slow and too
inflexible to route
WM_*messages. - Document/view,
neweverywhere, and Windows-only by construction. - No separation. The UI class is the logic class, so the whole program is untestable without a message pump.
MFC is the clearest cautionary tale in the language: it invented its own RTTI
(CRuntimeClass), its own dispatch (message maps) and its own ownership model,
each to work around a cost of the C++ object model, and each became a permanent,
non-portable, un-testable layer.
4. The Bill
| Cost | Where it comes from |
|---|---|
| binary size: tens of MB before your first line of code | the framework is statically linked with all its widgets, resources, meta-objects and every virtual function, called or not (Ch18.4) |
| one heap object per widget, and a parent tree to free them | new in every constructor, ownership pushed into the base class |
| one ABI, one compiler, one set of flags, forever | non-header-only C++ with virtual interfaces |
| two type systems: C++'s and the framework's | moc/staticMetaObject and CRuntimeClass |
| untestable UI logic | logic lives in virtual handlers that need a window, a device and a pump |
| a UI you cannot update without shipping a new binary | the interface is compiled into the program |
5. What We Do Instead: A PWA and a Native Service
Split the program in two along a line that has nothing to do with C++:
- The user interface is a web application — HTML, CSS, JavaScript, a Progressive Web App that installs like a native app and updates itself. It has no C++ in it, no framework ABI, no virtual dispatch, and it runs in a sandbox that is far safer than any native toolkit.
- The C++ program is a service — a local or remote process with a
narrow interface (HTTP, a socket protocol, a shared-memory ring), built out of values,
type erasure and
fast_io. It can be tested with a text-based client, it starts in milliseconds, and its UI can be replaced without recompiling it.
This is not a compromise, it is strictly better on every axis this chapter has measured:
no new per widget, no virtual hierarchy, no RTTI, no framework ABI, no extra
type system, and the interface ships independently of the binary. Chapter 21 is about
making the second half — the service — a PWA that installs and works offline
(Ch21: PWAs).
If you genuinely need a native window, use the platform API directly
(CreateWindowEx and a message loop on Windows, Wayland/X11 on Linux) behind
your own thin wrapper. It is a few hundred lines, it has no object model, no moc, no
second RTTI, and fast_io already gives you the file handles and the
io_uring/IOCP plumbing to drive it
(Ch11.14).
6. Rules of Thumb
- Do not build a new program on Qt or MFC. Both are 1990s object models kept alive by inertia; you inherit every problem in this chapter and none of the performance.
- If a job description says "Qt developer", understand that you will be paid to maintain a virtual hierarchy and a moc build step — and that the skills do not transfer to anything else in C++.
- If you need cross-platform UI today: web front end, native service. If you need native UI: platform API, thin wrapper.
- Keep the UI out of the C++ program's type system entirely. The moment a
QWidget *appears in a function signature, your library depends on Qt and your tests need a display. - When you must consume a framework, contain it: one translation unit, one adapter class, values at the boundary. Never let its pointers escape into your own API.