Have you ever tried to look inside some object in the debugger
and been frustrated that the debugger shows the details of the object’s
physical implementation instead of the logical information that the object
is supposed to represent? Let me illustrate this using an example of a
Date
class that represents
calendar dates, such as December 26, 2011. If you look into this object in
the debugger, chances are you will not see anything resembling “December 26,
2011” or any human-readable information at all, but rather an integer that
requires some decoding to convert into a date it represents.
It all depends on how the Date
type is implemented. I
have seen the following three implementations:
The first implementation is pretty self-evident and is a pleasure to debug. In the second case, the date December 26, 2011 is represented by an integer 20111226, which is also easily readable by a human once you know the formula behind it.
In the last case, the internal representation of a Date
is the number of days
that have passed since some arbitrarily chosen date far enough in the past,
that the day represented by 1 is 1/1/1900 or 1/1/0000 or something of this
sort.
While the first two implementations are very debugger-friendly, they
have a serious problem. The Date
type is supposed to
support “date arithmetic,” i.e., operations such as adding a number of days
to a date, or calculating the number of days between two dates. In the cases
of implementations 1 and 2 such number arithmetic is extremely slow, while
in the case of implementation 3 it is as efficient as adding and subtracting
integers.
For this reason, any serious implementation of Date
uses approach 3.
However, when you look at this Date
object in the
debugger, it is a pain to figure out what the actual calendar date is. For
example, in the class Date
we will consider
momentarily, the date December 26, 2011 looks like 734497 in the debugger,
and when you are working with code that contains a lot of dates—for example,
some financial contract that pays quarterly for the next 30 years, and also
has some additional dates a couple of days before each payment date relevant
for calculation—debugging becomes a challenge.
But it doesn’t have to be. The solution to this problem is to make the
code of the class Date
“debugger-friendly,”
meaning that when compiled in debug mode, it provides additional information
in the debugger to represent the date in a human-readable form (either as
“December 26, 2011” or at least 20111226). However, given that this
additional functionality requires some calculations and increases the size
of the object, I’ve decided to compromise and settle on the second solution,
representing the debugging info of the date in YYYYMMDD format, i.e., as
20111226.
The complete source code for the class Date
is provided in Appendix J in the scpp_date.hpp and scpp_date.cpp files. Here I just include snippets
from these files that provide this additional debugging information. In the
header file we find:
class Date { public: // some code private: int date_; // number of days from A.D., i.e. 01/01/0000 is 1. #ifdef _DEBUG int yyyymmdd_; #endif void SyncDebug() { #ifdef _DEBUG yyyymmdd_ = AsYYYYMMDD(); #endif } void SyncDebug(unsigned year, unsigned month, unsigned day) { #ifdef _DEBUG yyyymmdd_ = 10000*year + 100*month + day; #endif } };
First, the implementation is based on a number of days since some day
in the past. In addition, when
compiled in debug mode, the symbol _DEBUG
is defined and the
class has an additional data member
int yyyymmdd_
, which will contain the
date in the YYYYMMDD format. To fill this data member out, there are two
functions SyncDebug()
, so named because they
synchronize the debug information with the actual date_
contained in the object. When compiled in
release mode, these two functions do nothing, and in debug mode they update
the yyyymmdd_
data member. These
functions are called from every non-const
method of the class after modifying the date_
data member, for example:
Date& operator ++ () { ++date_; SyncDebug(); return *this; } // some other non-const methods Date& operator += (int nDays) { date_ += nDays; SyncDebug(); return *this; } // even more non-const methods
and also in a constructor:
Date::Date(unsigned year, unsigned month, unsigned day) {
SCPP_TEST_ASSERT(year>=1900, "Year must be >=1900.")
SCPP_TEST_ASSERT(JAN<=month && month<=DEC,
"Wrong month " << month << " must be 1..12.")
#ifdef SCPP_TEST_ASSERT_ON
unsigned ml = MonthLength(month, year);
SCPP_TEST_ASSERT(1<=day && day<=ml,
"Wrong day: " << day << " must be 1.." << ml << ".");
#endif
int n_years_before = year - 1;
date_ = 365*n_years_before + n_years_before/4 - n_years_before/100
+ n_years_before/400 + day + NumberOfDaysBeforeMonth(month, year);
SyncDebug(year, month, day);
}
Figure 16-1 shows how the Date
object looks in the
XCode debugger as a result of all this additional activity in debug
mode.
The variable d
of type Date
is shown in the upper
right columns. In the “Arguments” column find d
, and under it you can see its data members,
while in the next column, “Values,” you can see that:
date_
is equal to
734497.
yyyymmdd_
is equal to
20111226.
The presence of the latter value makes decoding the date in the object as easy as separating the last two pairs of digits from the first four.
The example of the Date
class discussed here
is just that: an example of an approach to making your class friendly to a
debugger. I started to work on this mostly out of frustration when trying to
look into STL containers in the debugger and finding a lot of interesting
details about their implementation instead of what numbers or strings or
other objects they actually contained. Making STL containers
debugger-friendly on the level of code could be (and was) done, though it
makes the code compiled in debug mode exceptionally slow. However, this
problem was addressed recently on the level of the debugger: Microsoft
Visual Studio 2010 shows the logical contents (as opposed to implementation
details) of STL containers, such as a vector, set, or map (Figure 16-2).
Thus, there is hope that this idea will soon reach debuggers working under Unix, Linux, and Mac OS too.
In the case of a specific class you create, if its implementation differs from the logical information it represents, it is up to you to make it debugger-friendly. Usually it is not difficult, and you will be glad you did it as you debug your program.
18.221.165.115