After almost three decades of using OWL, I am finally starting to get a handle (ha ha) on how "orphan control" (object reference counting) in TDC and TGdiObject works, its purpose and weaknesses. I am currently considering removing the orphan control in Owlet (my experimental branch of OWLNext), so I have just been through a thorough study and review of the code.
Why remove orphan control? Well, in my pretty big application, with a lot of drawing that has to be compatible with both screen and printout, I have found that I can disable it without any bad effect. Why disable it? Well, obviously the Windows Graphics Device Interface designers intended GDI object selection to be fast. They separated the creation of drawing attribute objects, such as pens and brushes, from the selection of those objects, and from the drawing functions using those objects. This allowed very little overhead for selection and drawing functions, compared to a slower approach, such as specifying all the pen and brush attributes as parameters to each drawing operation. Unfortunately, the TDC reference counting slows down selection substantially, as every selection call requires table lookup to update reference counts.
There are also the issues of poor support for nesting and the lack of exception safety. Let's dive in and see if we can improve things!
Why does TDC do reference counting, a.k.a. "orphan control"? It has all to do with sloppy programming:
TPenDemo::DrawLine(TDC& dc, const TPoint& point, TColor& color)
{
TPen BrushPen(color, PenSize);
dc.SelectObject(BrushPen);
dc.LineTo(point);
}
Here, in this actual example from the documentation, the function returns with the dead pen still selected. Ouch! You can imagine the smell when TDC tries to draw something later! Fortunately, the reference counting in TDC prevents this morbid event from happening by keeping a reference to the underlying HPEN handle. Since the reference count does not go to zero at the end of the function, the handle is kept alive. It is now an "orphan". Its reference count goes to zero whenever TDC destructs and gives up its reference, and it is then finally deleted. This may also happen if there is another SelectObject call replacing the handle before then.
To avoid orphans piling up, the programming idiom taught by the OWL documentation is to use the TDC member functions for restoring objects (RestorePen, RestoreBrush, etc.). For example, here is an example from the documentation that gets it right:
void TMyWindow::PaintRect(TDC& dc, TPoint& p, TSize& size)
{
TBrush brush(TColor(5,5,5));
dc.SelectObject(brush);
dc.Rectangle(p, size);
dc.RestoreBrush();
}
The call to RestoreBrush will make TDC give up its reference to the brush, which means that the reference count goes to zero at the end of the function when the brush destructs, causing the handle to be deleted. As the function returns, the original brush is selected, and all is well.
Unfortunately, this select-and-restore idiom does not nest very well. The restore functions reset the selection to the original object at the time the TDC was constructed, or since the last call to the restore function. Unfortunately, they do not reset it to what it was before the last call to SelectObject. For example:
// Using RestoreFont to restore state will not work in nested cases.
const auto testRestoreApproach = [&]
{
const auto font1 = TFont {"Arial", fontSize};
const auto oldTextColor = dc.SetTextColor (TColor::LtRed);
dc.SelectObject (font1);
dc.TextOut (nextPos (), "font1 (RestoreFont approach)");
if (true)
{
const auto font2 = TFont {"Comic Sans MS", fontSize};
dc.SelectObject (font2);
dc.TextOut (nextPos (), "font2");
dc.RestoreFont ();
}
dc.TextOut (nextPos (), "font1, again? No, RestoreFont restored the default font.");
dc.RestoreFont ();
dc.SetTextColor (oldTextColor);
};
The way to deal with this is to always reselect the object immediately before drawing:
const auto testReselectApproach = [&]
{
const auto oldTextColor = dc.SetTextColor (TColor::LtGreen);
const auto font1 = TFont {"Arial", fontSize};
dc.SelectObject (font1);
dc.TextOut (nextPos (), "font1 (Reselect workaround)");
if (true)
{
const auto font2 = TFont {"Comic Sans MS", fontSize};
dc.SelectObject (font2);
dc.TextOut (nextPos (), "font2");
dc.RestoreFont ();
}
dc.SelectObject (font1);
dc.TextOut (nextPos (), "font1, again. Of course, we explicitly reselected it!");
dc.RestoreFont ();
dc.SetTextColor (oldTextColor);
};
This works. The selected font is always the one we want. But it is tedious. And it is easy to get wrong by forgetting to reselect the object. And it is slow. Here we have remembered to include all the restore calls, so that there are no orphans. Still we pay the price of reference counting. That is not in the C++ spirit. We shouldn't pay for something we don't need.
Can we find a better solution?
All the other state in TDC, such as text colour, is managed by the caller storing and restoring the previous setting (see example above). Unfortunately, this is not easy for pens, brushes and the other GDI objects, because TDC::SelectObject does not return anything (unlike the Windows API function it encapsulates, which returns the handle to the previously selected object). So we need to use the underlying Windows API and make sure we keep track of owned and non-owned objects:
// To make the manual restore approach work, we need to use the reference counted OWLNext
// objects (TPen, etc.) to encapsulate the handles. It is now very important to properly
// distinguish owned (reference counted) and non-owned (aliased) handles. The latter should
// not be counted!
//
// See [feature-requests:#175] for a proposal to make SelectObject return the replaced object,
// and encapsulate all this complexity.
const auto testManualRestore = [&]
{
const auto oldTextColor = dc.SetTextColor (TColor::LtBlue);
const auto oldFontHandle = reinterpret_cast <HFONT> (dc.GetCurrentObject (OBJ_FONT));
const auto isOldFontOwned = (TGdiObject::RefFind (oldFontHandle) != nullptr);
const auto oldFont = TFont {oldFontHandle, isOldFontOwned ? AutoDelete : NoAutoDelete};
const auto font1 = TFont {"Arial", fontSize};
dc.SelectObject (font1);
dc.TextOut (nextPos (), "font1 (Manual restore approach)");
if (true)
{
const auto oldFontHandle = reinterpret_cast <HFONT> (dc.GetCurrentObject (OBJ_FONT));
const auto isOldFontOwned = (TGdiObject::RefFind (oldFontHandle) != nullptr);
const auto oldFont = TFont {oldFontHandle, isOldFontOwned ? AutoDelete : NoAutoDelete};
const auto font2 = TFont {"Comic Sans MS", fontSize};
dc.SelectObject (font2);
dc.TextOut (nextPos (), "font2");
dc.SelectObject (oldFont);
}
dc.TextOut (nextPos (), "font1, again? Yes, after manual restore.");
const auto font3 = TFont {"Impact", fontSize};
dc.SelectObject (font3);
dc.TextOut (nextPos (), "font3");
dc.SelectObject (font1);
dc.TextOut (nextPos (), "font1, back again? Yes, all is good!");
dc.SelectObject (oldFont);
dc.SetTextColor (oldTextColor);
};
This is far too complicated and error-prone. To simplify it, I am proposing to change TDC::SelectObject to return the replaced object, and encapsulate all this complexity (see [feature-requests:#175]). With this extension in place, the manual approach becomes much simpler, with object selection and restoration now consistent with setting other DC attributes and restoring them, such as text colour.
const auto testNewManualRestore = [&]
{
const auto oldTextColor = dc.SetTextColor (TColor::LtBlue);
const auto font1 = TFont {"Arial", fontSize};
const auto oldFont = dc.SelectObject (font1);
dc.TextOut (nextPos (), "font1 (New manual restore approach)");
if (true)
{
const auto font2 = TFont {"Comic Sans MS", fontSize};
const auto oldFont = dc.SelectObject (font2);
dc.TextOut (nextPos (), "font2");
dc.SelectObject (oldFont);
}
dc.TextOut (nextPos (), "font1, again? Yes, after manual restore.");
const auto font3 = TFont {"Impact", fontSize};
dc.SelectObject (font3);
dc.TextOut (nextPos (), "font3");
dc.SelectObject (font1);
dc.TextOut (nextPos (), "font1, back again? Yes, all is good!");
dc.SelectObject (oldFont);
dc.SetTextColor (oldTextColor);
}
With this basic functionality, we now have a workable low-level approach. But this is still error-prone, and it is not exception safe. For really robust code that is written to deal with errors, we need an approach that works with exceptions.
With SelectObject returning the replaced object, we can use an RAII scope guard, such as std::unique_ptr, to ensure that the replaced object is restored in case an exception occurs. Here is the code above with std::unique_ptr as a scope guard:
// We can use std::unique_ptr as an RAII guard for exception safety.
const auto testRaiiRestoreApproach = [&]
{
const auto oldTextColor = dc.SetTextColor (TColor::MediumBlue);
const auto oldTextColorRestore = [&dc] (auto p) { dc.SetTextColor (*p); };
const auto oldTextColorGuard = unique_ptr <const TColor, decltype (oldTextColorRestore)> {&oldTextColor, oldTextColorRestore};
const auto font1 = TFont {"Arial", fontSize};
const auto oldFont = dc.SelectObject (font1);
const auto oldFontRestore = [&dc] (auto p) { dc.SelectObject (*p); };
const auto oldFontGuard = unique_ptr <const TFont, decltype (oldFontRestore)> {&oldFont, oldFontRestore};
dc.TextOut (nextPos (), "font1 (RAII guard approach)");
if (true)
{
const auto font2 = TFont {"Comic Sans MS", fontSize};
const auto oldFont = dc.SelectObject (font2);
const auto oldFontRestore = [&dc] (auto p) { dc.SelectObject (*p); };
const auto oldFontGuard = unique_ptr <const TFont, decltype (oldFontRestore)> {&oldFont, oldFontRestore};
dc.TextOut (nextPos (), "font2");
}
dc.TextOut (nextPos (), "font1, again? Yes, after manual restore.");
const auto font3 = TFont {"Impact", fontSize};
dc.SelectObject (font3);
dc.TextOut (nextPos (), "font3");
dc.SelectObject (font1);
dc.TextOut (nextPos (), "font1, back again? Yes, all is good!");
};
With this in place, we could remove the TDC reference counting altogether.
However, this approach is very tedious, and safety would very much rely on programmer discipline, as including scope guards manually is easy to forget. We can of course write utility classes that encapsulate the setting and restoring of GDI state, by selecting the object in the constructor and restoring the replaced one in the destructor. That can be helpful, but let's look at an even simpler alternative.
While researching the TDC orphan control (reference counting) for [feature-requests:#175], I discovered this interesting approach that will properly restore selections to the previous state, not back to the original state, and that hence will nest nicely:
// A trick is to use a TDC alias as a scope guard. The alias will keep track of object
// selection within the scope, and restore the original objects at the end of the scope.
//
// A big problem with this approach is that it circumvents virtual overrides in the aliased
// TDC, so the drawing will not work with e.g. TPrintPreviewDC which depends on overriding
// SelectObject and other functions in TDC.
const auto testAliasApproach = [&]
{
auto d = TDC {dc.GetHDC ()}; // Keeps track of objects in this scope.
const auto oldTextColor = d.SetTextColor (TColor::LtMagenta);
const auto font1 = TFont {"Arial", fontSize};
d.SelectObject (font1);
d.TextOut (nextPos (), "font1 (TDC alias approach)");
if (true)
{
auto d = TDC {dc.GetHDC ()}; // Create a new alias for this scope.
const auto font2 = TFont {"Comic Sans MS", fontSize};
d.SelectObject (font2);
d.TextOut (nextPos (), "font2");
}
d.TextOut (nextPos (), "font1, again? Yes, the TDC alias restored it at destruction.");
d.SetTextColor (oldTextColor);
};
Here, the TDC alias (non-owning encapsulation of the HDC handle), will restore the selections at the end of the scope, when it is destructed. The selections are restored to the state when the TDC alias was created, so nesting works well. Unlike the taught idiom, it is also exception safe! In case of an exception, the TDC alias is destructed, and the previous selections are restored.
The only (but big) problem with this approach, that I can see, is that virtual functions in the aliased TDC ("dc" above) are circumvented. So this approach does not work with derivatives of TDC, such as TPrintPreviewDC, that rely on overriding TDC virtual functions.
Rather than use a TDC alias as an improvised state guard, we could create a dedicated state guard, such as the proposed TDC::TStateGuard [feature-requests:#176], that does GDI state tracking within the associated scope. This should be simple to implement by using TDC::SaveDC in the guard's constructor and TDC::RestoreDC in its destructor. This solution would nest well, would be exception-safe, and would not suffer from the virtual function problem with the TDC alias approach.
For example:
// The new TDC::TStateGuard does GDI state tracking within the associated scope, and is hence
// nesting friendly and exception safe.
const auto testStateGuardApproach = [&]
{
auto sg = TDC::TStateGuard {dc}; // Keeps track of state in this scope.
dc.SetTextColor (TColor::Gray);
const auto font1 = TFont {"Arial", fontSize};
dc.SelectObject (font1);
dc.TextOut (nextPos (), "font1 (TDC::TStateGuard approach)");
if (true)
{
auto sg = TDC::TStateGuard {dc}; // Create another guard for this scope.
const auto font2 = TFont {"Comic Sans MS", fontSize};
dc.SelectObject (font2);
dc.TextOut (nextPos (), "font2");
}
dc.TextOut (nextPos (), "font1, again? Yes, the guard restored it at destruction.");
};
The only problem with this solution, that I can see, is that it may be relatively costly to save and restore the full TDC state in every scope. That said, the programmer can optimise this by not using the scope guard for speed critical code, and instead use more specific RAII classes, or refactor the code to move the selection part out of the speed critical code altogether, thus eliminate the need for the state guard.
With the consistent use of TDC::TStateGuard, combined with RAII classes wrapping the manual approach, where needed for speed, we have an exception safe solution, which means that the orphan control in TDC can be removed. This may gain some speed, but more importantly, it will make TDC more robust and simplify maintenance. While the long-standing bug in TDC::RestoreDC [bugs:#494] was recently fixed in Owlet, there are remaining functions known to be incompatible with the orphan control, such as TDC::ResetDC and the OWLNext extension TDC::SetDC (now removed in Owlet).
This RAII state guard solution is currently being trialled in Owlet, and if it works out, it may be proposed for OWLNext.
Let me know what you think in our discussion forum [discussion:00f93e1a3a], where you will also find a test project including the code examples used above.
Bugs: #494
Discussion: 00f93e1a3a
Discussion: Selecting and restoring objects in TDC
Discussion: Selecting and restoring objects in TDC
Feature Requests: #175
Feature Requests: #176
Feature Requests: #178
Wiki: Knowledge_Base